/system/classes/kohana/request.php
PHP | 1305 lines | 702 code | 163 blank | 440 comment | 69 complexity | b303deeee8c1c7001146d9134382b3cb MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-2.1
- <?php defined('SYSPATH') or die('No direct script access.');
- /**
- * Request and response wrapper. Uses the [Route] class to determine what
- * [Controller] to send the request to.
- *
- * @package Kohana
- * @category Base
- * @author Kohana Team
- * @copyright (c) 2008-2010 Kohana Team
- * @license http://kohanaframework.org/license
- */
- class Kohana_Request {
- /**
- * @var array 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',
- 207 => 'Multi-Status',
- // 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',
- 422 => 'Unprocessable Entity',
- 423 => 'Locked',
- 424 => 'Failed Dependency',
- // 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',
- 509 => 'Bandwidth Limit Exceeded'
- );
- /**
- * @var string method: GET, POST, PUT, DELETE, etc
- */
- public static $method = 'GET';
- /**
- * @var string protocol: http, https, ftp, cli, etc
- */
- public static $protocol = 'http';
- /**
- * @var string referring URL
- */
- public static $referrer;
- /**
- * @var string client user agent
- */
- public static $user_agent = '';
- /**
- * @var string client IP address
- */
- public static $client_ip = '0.0.0.0';
- /**
- * @var boolean AJAX-generated request
- */
- public static $is_ajax = FALSE;
- /**
- * @var Request main request instance
- */
- public static $instance;
- /**
- * @var Request currently executing request instance
- */
- public static $current;
- /**
- * Main request singleton instance. If no URI is provided, the URI will
- * be automatically detected.
- *
- * $request = Request::instance();
- *
- * @param string URI of the request
- * @return Request
- * @uses Request::detect_uri
- */
- public static function instance( & $uri = TRUE)
- {
- if ( ! Request::$instance)
- {
- if (Kohana::$is_cli)
- {
- // Default protocol for command line is cli://
- Request::$protocol = 'cli';
- // Get the command line options
- $options = CLI::options('uri', 'method', 'get', 'post');
- if (isset($options['uri']))
- {
- // Use the specified URI
- $uri = $options['uri'];
- }
- if (isset($options['method']))
- {
- // Use the specified method
- Request::$method = strtoupper($options['method']);
- }
- if (isset($options['get']))
- {
- // Overload the global GET data
- parse_str($options['get'], $_GET);
- }
- if (isset($options['post']))
- {
- // Overload the global POST data
- parse_str($options['post'], $_POST);
- }
- }
- else
- {
- if (isset($_SERVER['REQUEST_METHOD']))
- {
- // Use the server request method
- Request::$method = $_SERVER['REQUEST_METHOD'];
- }
- if ( ! empty($_SERVER['HTTPS']) AND filter_var($_SERVER['HTTPS'], FILTER_VALIDATE_BOOLEAN))
- {
- // This request is secure
- Request::$protocol = 'https';
- }
- if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')
- {
- // This request is an AJAX request
- Request::$is_ajax = TRUE;
- }
- if (isset($_SERVER['HTTP_REFERER']))
- {
- // There is a referrer for this request
- Request::$referrer = $_SERVER['HTTP_REFERER'];
- }
- if (isset($_SERVER['HTTP_USER_AGENT']))
- {
- // Set the client user agent
- Request::$user_agent = $_SERVER['HTTP_USER_AGENT'];
- }
- if (isset($_SERVER['HTTP_X_FORWARDED_FOR']))
- {
- // Use the forwarded IP address, typically set when the
- // client is using a proxy server.
- Request::$client_ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
- }
- elseif (isset($_SERVER['HTTP_CLIENT_IP']))
- {
- // Use the forwarded IP address, typically set when the
- // client is using a proxy server.
- Request::$client_ip = $_SERVER['HTTP_CLIENT_IP'];
- }
- elseif (isset($_SERVER['REMOTE_ADDR']))
- {
- // The remote IP address
- Request::$client_ip = $_SERVER['REMOTE_ADDR'];
- }
- if (Request::$method !== 'GET' AND Request::$method !== 'POST')
- {
- // Methods besides GET and POST do not properly parse the form-encoded
- // query string into the $_POST array, so we overload it manually.
- parse_str(file_get_contents('php://input'), $_POST);
- }
- if ($uri === TRUE)
- {
- $uri = Request::detect_uri();
- }
- }
- // Reduce multiple slashes to a single slash
- $uri = preg_replace('#//+#', '/', $uri);
- // Remove all dot-paths from the URI, they are not valid
- $uri = preg_replace('#\.[\s./]*/#', '', $uri);
- // Create the instance singleton
- Request::$instance = Request::$current = new Request($uri);
- // Add the default Content-Type header
- Request::$instance->headers['Content-Type'] = 'text/html; charset='.Kohana::$charset;
- }
- return Request::$instance;
- }
- /**
- * Automatically detects the URI of the main request using PATH_INFO,
- * REQUEST_URI, PHP_SELF or REDIRECT_URL.
- *
- * $uri = Request::detect_uri();
- *
- * @return string URI of the main request
- * @throws Kohana_Exception
- * @since 3.0.8
- */
- public static function detect_uri()
- {
- if ( ! empty($_SERVER['PATH_INFO']))
- {
- // PATH_INFO does not contain the docroot or index
- $uri = $_SERVER['PATH_INFO'];
- }
- else
- {
- // REQUEST_URI and PHP_SELF include the docroot and index
- if (isset($_SERVER['REQUEST_URI']))
- {
- // REQUEST_URI includes the query string, remove it
- $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
- // Decode the request URI
- $uri = rawurldecode($uri);
- }
- elseif (isset($_SERVER['PHP_SELF']))
- {
- $uri = $_SERVER['PHP_SELF'];
- }
- elseif (isset($_SERVER['REDIRECT_URL']))
- {
- $uri = $_SERVER['REDIRECT_URL'];
- }
- else
- {
- // If you ever see this error, please report an issue at http://dev.kohanaphp.com/projects/kohana3/issues
- // along with any relevant information about your web server setup. Thanks!
- throw new Kohana_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, PHP_SELF or REDIRECT_URL');
- }
- // Get the path from the base URL, including the index file
- $base_url = parse_url(Kohana::$base_url, PHP_URL_PATH);
- if (strpos($uri, $base_url) === 0)
- {
- // Remove the base URL from the URI
- $uri = (string) substr($uri, strlen($base_url));
- }
- if (Kohana::$index_file AND strpos($uri, Kohana::$index_file) === 0)
- {
- // Remove the index file from the URI
- $uri = (string) substr($uri, strlen(Kohana::$index_file));
- }
- }
- return $uri;
- }
- /**
- * Return the currently executing request. This is changed to the current
- * request when [Request::execute] is called and restored when the request
- * is completed.
- *
- * $request = Request::current();
- *
- * @return Request
- * @since 3.0.5
- */
- public static function current()
- {
- return Request::$current;
- }
- /**
- * Creates a new request object for the given URI. This differs from
- * [Request::instance] in that it does not automatically detect the URI
- * and should only be used for creating HMVC requests.
- *
- * $request = Request::factory($uri);
- *
- * @param string URI of the request
- * @return Request
- */
- public static function factory($uri)
- {
- return new Request($uri);
- }
- /**
- * Returns information about the client user agent.
- *
- * // Returns "Chrome" when using Google Chrome
- * $browser = Request::user_agent('browser');
- *
- * Multiple values can be returned at once by using an array:
- *
- * // Get the browser and platform with a single call
- * $info = Request::user_agent(array('browser', 'platform'));
- *
- * When using an array for the value, an associative array will be returned.
- *
- * @param mixed string to return: browser, version, robot, mobile, platform; or array of values
- * @return mixed requested information, FALSE if nothing is found
- * @uses Kohana::config
- * @uses Request::$user_agent
- */
- public static function user_agent($value)
- {
- if (is_array($value))
- {
- $agent = array();
- foreach ($value as $v)
- {
- // Add each key to the set
- $agent[$v] = Request::user_agent($v);
- }
- return $agent;
- }
- static $info;
- if (isset($info[$value]))
- {
- // This value has already been found
- return $info[$value];
- }
- if ($value === 'browser' OR $value == 'version')
- {
- // Load browsers
- $browsers = Kohana::config('user_agents')->browser;
- foreach ($browsers as $search => $name)
- {
- if (stripos(Request::$user_agent, $search) !== FALSE)
- {
- // Set the browser name
- $info['browser'] = $name;
- if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches))
- {
- // Set the version number
- $info['version'] = $matches[1];
- }
- else
- {
- // No version number found
- $info['version'] = FALSE;
- }
- return $info[$value];
- }
- }
- }
- else
- {
- // Load the search group for this type
- $group = Kohana::config('user_agents')->$value;
- foreach ($group as $search => $name)
- {
- if (stripos(Request::$user_agent, $search) !== FALSE)
- {
- // Set the value name
- return $info[$value] = $name;
- }
- }
- }
- // The value requested could not be found
- return $info[$value] = FALSE;
- }
- /**
- * Returns the accepted content types. If a specific type is defined,
- * the quality of that type will be returned.
- *
- * $types = Request::accept_type();
- *
- * @param string content MIME type
- * @return float when checking a specific type
- * @return array
- * @uses Request::_parse_accept
- */
- public static function accept_type($type = NULL)
- {
- static $accepts;
- if ($accepts === NULL)
- {
- // Parse the HTTP_ACCEPT header
- $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT'], array('*/*' => 1.0));
- }
- if (isset($type))
- {
- // Return the quality setting for this type
- return isset($accepts[$type]) ? $accepts[$type] : $accepts['*/*'];
- }
- return $accepts;
- }
- /**
- * Returns the accepted languages. If a specific language is defined,
- * the quality of that language will be returned. If the language is not
- * accepted, FALSE will be returned.
- *
- * $langs = Request::accept_lang();
- *
- * @param string language code
- * @return float when checking a specific language
- * @return array
- * @uses Request::_parse_accept
- */
- public static function accept_lang($lang = NULL)
- {
- static $accepts;
- if ($accepts === NULL)
- {
- // Parse the HTTP_ACCEPT_LANGUAGE header
- $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_LANGUAGE']);
- }
- if (isset($lang))
- {
- // Return the quality setting for this lang
- return isset($accepts[$lang]) ? $accepts[$lang] : FALSE;
- }
- return $accepts;
- }
- /**
- * Returns the accepted encodings. If a specific encoding is defined,
- * the quality of that encoding will be returned. If the encoding is not
- * accepted, FALSE will be returned.
- *
- * $encodings = Request::accept_encoding();
- *
- * @param string encoding type
- * @return float when checking a specific encoding
- * @return array
- * @uses Request::_parse_accept
- */
- public static function accept_encoding($type = NULL)
- {
- static $accepts;
- if ($accepts === NULL)
- {
- // Parse the HTTP_ACCEPT_LANGUAGE header
- $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_ENCODING']);
- }
- if (isset($type))
- {
- // Return the quality setting for this type
- return isset($accepts[$type]) ? $accepts[$type] : FALSE;
- }
- return $accepts;
- }
- /**
- * Parses an accept header and returns an array (type => quality) of the
- * accepted types, ordered by quality.
- *
- * $accept = Request::_parse_accept($header, $defaults);
- *
- * @param string header to parse
- * @param array default values
- * @return array
- */
- protected static function _parse_accept( & $header, array $accepts = NULL)
- {
- if ( ! empty($header))
- {
- // Get all of the types
- $types = explode(',', $header);
- foreach ($types as $type)
- {
- // Split the type into parts
- $parts = explode(';', $type);
- // Make the type only the MIME
- $type = trim(array_shift($parts));
- // Default quality is 1.0
- $quality = 1.0;
- foreach ($parts as $part)
- {
- // Prevent undefined $value notice below
- if (strpos($part, '=') === FALSE)
- continue;
- // Separate the key and value
- list ($key, $value) = explode('=', trim($part));
- if ($key === 'q')
- {
- // There is a quality for this type
- $quality = (float) trim($value);
- }
- }
- // Add the accept type and quality
- $accepts[$type] = $quality;
- }
- }
- // Make sure that accepts is an array
- $accepts = (array) $accepts;
- // Order by quality
- arsort($accepts);
- return $accepts;
- }
- /**
- * @var object route matched for this request
- */
- public $route;
- /**
- * @var integer HTTP response code: 200, 404, 500, etc
- */
- public $status = 200;
- /**
- * @var string response body
- */
- public $response = '';
- /**
- * @var array headers to send with the response body
- */
- public $headers = array();
- /**
- * @var string controller directory
- */
- public $directory = '';
- /**
- * @var string controller to be executed
- */
- public $controller;
- /**
- * @var string action to be executed in the controller
- */
- public $action;
- /**
- * @var string the URI of the request
- */
- public $uri;
- // Parameters extracted from the route
- protected $_params;
- /**
- * Creates a new request object for the given URI. New requests should be
- * created using the [Request::instance] or [Request::factory] methods.
- *
- * $request = new Request($uri);
- *
- * @param string URI of the request
- * @return void
- * @throws Kohana_Request_Exception
- * @uses Route::all
- * @uses Route::matches
- */
- public function __construct($uri)
- {
- // Remove trailing slashes from the URI
- $uri = trim($uri, '/');
- // Load routes
- $routes = Route::all();
- foreach ($routes as $name => $route)
- {
- if ($params = $route->matches($uri))
- {
- // Store the URI
- $this->uri = $uri;
- // Store the matching route
- $this->route = $route;
- if (isset($params['directory']))
- {
- // Controllers are in a sub-directory
- $this->directory = $params['directory'];
- }
- // Store the controller
- $this->controller = $params['controller'];
- if (isset($params['action']))
- {
- // Store the action
- $this->action = $params['action'];
- }
- else
- {
- // Use the default action
- $this->action = Route::$default_action;
- }
- // These are accessible as public vars and can be overloaded
- unset($params['controller'], $params['action'], $params['directory']);
- // Params cannot be changed once matched
- $this->_params = $params;
- return;
- }
- }
- // No matching route for this URI
- $this->status = 404;
- throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri',
- array(':uri' => $uri));
- }
- /**
- * Returns the response as the string representation of a request.
- *
- * echo $request;
- *
- * @return string
- */
- public function __toString()
- {
- return (string) $this->response;
- }
- /**
- * Generates a relative URI for the current route.
- *
- * $request->uri($params);
- *
- * @param array additional route parameters
- * @return string
- * @uses Route::uri
- */
- public function uri(array $params = NULL)
- {
- if ( ! isset($params['directory']))
- {
- // Add the current directory
- $params['directory'] = $this->directory;
- }
- if ( ! isset($params['controller']))
- {
- // Add the current controller
- $params['controller'] = $this->controller;
- }
- if ( ! isset($params['action']))
- {
- // Add the current action
- $params['action'] = $this->action;
- }
- // Add the current parameters
- $params += $this->_params;
- return $this->route->uri($params);
- }
- /**
- * Create a URL from the current request. This is a shortcut for:
- *
- * echo URL::site($this->request->uri($params), $protocol);
- *
- * @param string route name
- * @param array URI parameters
- * @param mixed protocol string or boolean, adds protocol and domain
- * @return string
- * @since 3.0.7
- * @uses URL::site
- */
- public function url(array $params = NULL, $protocol = NULL)
- {
- // Create a URI with the current route and convert it to a URL
- return URL::site($this->uri($params), $protocol);
- }
- /**
- * Retrieves a value from the route parameters.
- *
- * $id = $request->param('id');
- *
- * @param string key of the value
- * @param mixed default value if the key is not set
- * @return mixed
- */
- public function param($key = NULL, $default = NULL)
- {
- if ($key === NULL)
- {
- // Return the full array
- return $this->_params;
- }
- return isset($this->_params[$key]) ? $this->_params[$key] : $default;
- }
- /**
- * Sends the response status and all set headers. The current server
- * protocol (HTTP/1.0 or HTTP/1.1) will be used when available. If not
- * available, HTTP/1.1 will be used.
- *
- * $request->send_headers();
- *
- * @return $this
- * @uses Request::$messages
- */
- public function send_headers()
- {
- if ( ! headers_sent())
- {
- if (isset($_SERVER['SERVER_PROTOCOL']))
- {
- // Use the default server protocol
- $protocol = $_SERVER['SERVER_PROTOCOL'];
- }
- else
- {
- // Default to using newer protocol
- $protocol = 'HTTP/1.1';
- }
- // HTTP status line
- header($protocol.' '.$this->status.' '.Request::$messages[$this->status]);
- foreach ($this->headers as $name => $value)
- {
- if (is_string($name))
- {
- // Combine the name and value to make a raw header
- $value = "{$name}: {$value}";
- }
- // Send the raw header
- header($value, TRUE);
- }
- }
- return $this;
- }
- /**
- * Redirects as the request response. If the URL does not include a
- * protocol, it will be converted into a complete URL.
- *
- * $request->redirect($url);
- *
- * [!!] No further processing can be done after this method is called!
- *
- * @param string redirect location
- * @param integer status code: 301, 302, etc
- * @return void
- * @uses URL::site
- * @uses Request::send_headers
- */
- public function redirect($url = '', $code = 302)
- {
- if (strpos($url, '://') === FALSE)
- {
- // Make the URI into a URL
- $url = URL::site($url, TRUE);
- }
- // Set the response status
- $this->status = $code;
- // Set the location header
- $this->headers['Location'] = $url;
- // Send headers
- $this->send_headers();
- // Stop execution
- exit;
- }
- /**
- * 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 with path, or TRUE for the current response
- * @param string downloaded file name
- * @param array 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->response;
- // 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
- $mime = File::mime($filename);
- }
- // 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->headers['Content-Range'] = 'bytes '.$start.'-'.$end.'/'.$size;
- $this->headers['Accept-Ranges'] = 'bytes';
- }
- // Set the headers for a download
- $this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
- $this->headers['Content-Type'] = $mime;
- $this->headers['Content-Length'] = ($end - $start) + 1;
- if (Request::user_agent('browser') === 'Internet Explorer')
- {
- // Naturally, IE does not act like a real browser...
- if (Request::$protocol === 'https')
- {
- // http://support.microsoft.com/kb/316431
- $this->headers['Pragma'] = $this->headers['Cache-Control'] = 'public';
- }
- if (version_compare(Request::user_agent('version'), '8.0', '>='))
- {
- // http://ajaxian.com/archives/ie-8-security
- $this->headers['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(Kohana::ERROR, $error);
- // Make sure the logs are written
- Kohana::$log->write();
- }
- // Do NOT display the exception, it will corrupt the output!
- }
- }
- // Stop execution
- exit;
- }
- /**
- * Parse the byte ranges from the HTTP_RANGE header used for
- * resumable downloads.
- *
- * @see 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);
- }
- /**
- * Processes the request, executing the controller action that handles this
- * request, determined by the [Route].
- *
- * 1. Before the controller action is called, the [Controller::before] method
- * will be called.
- * 2. Next the controller action will be called.
- * 3. After the controller action is called, the [Controller::after] method
- * will be called.
- *
- * By default, the output from the controller is captured and returned, and
- * no headers are sent.
- *
- * $request->execute();
- *
- * @return $this
- * @throws Kohana_Exception
- * @uses [Kohana::$profiling]
- * @uses [Profiler]
- */
- public function execute()
- {
- // Create the class prefix
- $prefix = 'controller_';
- if ($this->directory)
- {
- // Add the directory name to the class prefix
- $prefix .= str_replace(array('\\', '/'), '_', trim($this->directory, '/')).'_';
- }
- if (Kohana::$profiling)
- {
- // Set the benchmark name
- $benchmark = '"'.$this->uri.'"';
- if ($this !== Request::$instance AND Request::$current)
- {
- // Add the parent request uri
- $benchmark .= ' ÂŤ "'.Request::$current->uri.'"';
- }
- // Start benchmarking
- $benchmark = Profiler::start('Requests', $benchmark);
- }
- // Store the currently active request
- $previous = Request::$current;
- // Change the current request to this request
- Request::$current = $this;
- try
- {
- // Load the controller using reflection
- $class = new ReflectionClass($prefix.$this->controller);
- if ($class->isAbstract())
- {
- throw new Kohana_Exception('Cannot create instances of abstract :controller',
- array(':controller' => $prefix.$this->controller));
- }
- // Create a new instance of the controller
- $controller = $class->newInstance($this);
- // Execute the "before action" method
- $class->getMethod('before')->invoke($controller);
- // Determine the action to use
- $action = empty($this->action) ? Route::$default_action : $this->action;
- // Execute the main action with the parameters
- $class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params);
- // Execute the "after action" method
- $class->getMethod('after')->invoke($controller);
- }
- catch (Exception $e)
- {
- // Restore the previous request
- Request::$current = $previous;
- if (isset($benchmark))
- {
- // Delete the benchmark, it is invalid
- Profiler::delete($benchmark);
- }
- if ($e instanceof ReflectionException)
- {
- // Reflection will throw exceptions for missing classes or actions
- $this->status = 404;
- }
- else
- {
- // All other exceptions are PHP/server errors
- $this->status = 500;
- }
- // Re-throw the exception
- throw $e;
- }
- // Restore the previous request
- Request::$current = $previous;
- if (isset($benchmark))
- {
- // Stop the benchmark
- Profiler::stop($benchmark);
- }
- return $this;
- }
- /**
- * Generates an [ETag](http://en.wikipedia.org/wiki/HTTP_ETag) from the
- * request response.
- *
- * $etag = $request->generate_etag();
- *
- * [!!] If the request response is empty when this method is called, an
- * exception will be thrown!
- *
- * @return string
- * @throws Kohana_Request_Exception
- */
- public function generate_etag()
- {
- if ($this->response === NULL)
- {
- throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag');
- }
- // Generate a unique hash for the response
- return '"'.sha1($this->response).'"';
- }
- /**
- * Checks the browser cache to see the response needs to be returned.
- *
- * $request->check_cache($etag);
- *
- * [!!] If the cache check succeeds, no further processing can be done!
- *
- * @param string etag to check
- * @return $this
- * @throws Kohana_Request_Exception
- * @uses Request::generate_etag
- */
- public function check_cache($etag = null)
- {
- if (empty($etag))
- {
- $etag = $this->generate_etag();
- }
- // Set the ETag header
- $this->headers['ETag'] = $etag;
- // Add the Cache-Control header if it is not already set
- // This allows etags to be used with Max-Age, etc
- $this->headers += array(
- 'Cache-Control' => 'must-revalidate',
- );
- if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag)
- {
- // No need to send data again
- $this->status = 304;
- $this->send_headers();
- // Stop execution
- exit;
- }
- return $this;
- }
- } // End Request