<?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-2009 Kohana Team
* @license http://kohanaphp.com/license
*/
class Kohana_Request {
// 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 object main request instance
*/
public static $instance;
/**
* @var object currently executing request instance
*/
public static $current;
/**
* Main request singleton instance. If no URI is provided, the URI will
* be automatically detected using PATH_INFO, REQUEST_URI, or PHP_SELF.
*
* $request = Request::instance();
*
* @param string URI of the request
* @return Request
*/
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)
{
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);
}
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 and include a dump of $_SERVER
// http://dev.kohanaphp.com/projects/kohana3/issues
throw new Kohana_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, or PHP_SELF');
}
// 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 = substr($uri, strlen($base_url));
}
if (Kohana::$index_file AND strpos($uri, Kohana::$index_file) === 0)
{
// Remove the index file from the URI
$uri = substr($uri, strlen(Kohana::$index_file));
}
}
}
}
// 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;
}
/**
* 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');
*
* @param string value to return: browser, version, robot, mobile, platform
* @return string requested information
* @return FALSE no information found
* @uses Kohana::config
* @uses Request::$user_agent
*/
public static function user_agent($value)
{
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);
}
/**
* 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)));
}
// Get the content size
$size = strlen($this->response);
// Create a temporary file to hold the current response
$file = tmpfile();
// Write the current response into the file
fwrite($file, $this->response);
// Prepare the file for reading
fseek($file, 0);
}
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');
}
// Inline or download?
$disposition = empty($options['inline']) ? 'attachment' : 'inline';
// Set the headers for a download
$this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"';
$this->headers['Content-Type'] = $mime;
$this->headers['Content-Length'] = $size;
if ( ! empty($options['resumable']))
{
// @todo: ranged download processing
}
// 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);
// Keep the script running forever
set_time_limit(0);
// Send data in 16kb blocks
$block = 1024 * 16;
while ( ! feof($file))
{
if (connection_aborted())
break;
// 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;
}
/**
* 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