.
*/
/**
* Inspects the current HTTP request.
*
* Handles content negotiations and extracting data from headers safely on
* different web servers.
*
* @since 4.6.0
* @package HTTP
*/
namespace Textpattern\Http;
class Request
{
/**
* Protocol-port map.
*
* @var array
*/
protected $protocolMap = array(
'http' => 80,
'https' => 443,
);
/**
* Stores headers.
*
* @var array
*/
protected $headers;
/**
* Magic quotes GCP.
*
* @var bool
*/
protected $magicQuotesGpc = false;
/**
* Stores referer.
*
* @var string
*/
protected $referer;
/**
* Content types accepted by the client.
*
* @var array
*/
protected $acceptedTypes;
/**
* Formats mapping.
*
* @var array
*/
protected $acceptsFormats = array(
'html' => array('text/html', 'application/xhtml+xml', '*/*'),
'txt' => array('text/plain', '*/*'),
'js' => array('application/javascript', 'application/x-javascript', 'text/javascript', 'application/ecmascript', 'application/x-ecmascript', '*/*'),
'css' => array('text/css', '*/*'),
'json' => array('application/json', 'application/x-json', '*/*'),
'xml' => array('text/xml', 'application/xml', 'application/x-xml', '*/*'),
'rdf' => array('application/rdf+xml', '*/*'),
'atom' => array('application/atom+xml', '*/*'),
'rss' => array('application/rss+xml', '*/*'),
);
/**
* Raw request data.
*
* Wraps around PHP's $_SERVER variable.
*
* @var \Textpattern\Server\Config
*/
protected $request;
/**
* Constructor.
*
*
* echo Txp::get('\Textpattern\Http\Request', new Abc_Custom_Request_Data)->getHostName();
*
*
* @param \Textpattern\Server\Config|null $request The raw request data, defaults to the current request body
*/
public function __construct(\Textpattern\Server\Config $request = null)
{
if ($request === null) {
$this->request = \Txp::get('\Textpattern\Server\Config');
} else {
$this->request = $request;
}
$this->magicQuotesGpc = $this->request->getMagicQuotesGpc();
}
/**
* Checks whether the client accepts a certain response format.
*
* By default discards formats with quality factors below an arbitrary
* threshold as jQuery adds a wildcard content-type with quality of '0.01'
* to the 'Accept' header for XHR requests.
*
* Supplied format of 'html', 'txt', 'js', 'css', 'json', 'xml', 'rdf',
* 'atom' or 'rss' is autocompleted and matched againsts multiple valid MIMEs.
*
* Both of the following will return MIME for JSON if 'json' format is
* supported:
*
*
* echo Txp::get('\Textpattern\Http\Request')->getAcceptedType('json');
* echo Txp::get('\Textpattern\Http\Request')->getAcceptedType('application/json');
*
*
* The method can also be used to check an array of types:
*
*
* echo Txp::get('\Textpattern\Http\Request')->getAcceptedType(array('application/xml', 'application/x-xml'));
*
*
* Stops on first accepted format.
*
* @param string|array $formats Format to check
* @param float $threshold Quality threshold
* @return string|bool Supported type, or FALSE if not
*/
public function getAcceptedType($formats, $threshold = 0.1)
{
if ($this->acceptedTypes === null) {
$this->acceptedTypes = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT'));
}
foreach ((array) $formats as $format) {
if (isset($this->acceptsFormats[$format])) {
$format = $this->acceptsFormats[$format];
}
foreach ((array) $format as $type) {
if (isset($this->acceptedTypes[$type]) && $this->acceptedTypes[$type]['q'] >= $threshold) {
return $type;
}
}
}
return false;
}
/**
* Gets accepted language.
*
* If $languages is NULL, returns client's favoured language. If
* string, checks whether the language is supported and
* if an array, returns the language that the client favours the most.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getAcceptedLanguage('fi-FI');
*
*
* The above will return 'fi-FI' as long as the Accept-Language header
* contains an indentifier that matches Finnish, such as 'fi-fi', 'fi-Fi'
* or 'fi'.
*
* @param string|array $languages Languages to check
* @param float $threshold Quality threshold
* @return string|bool Accepted language, or FALSE
*/
public function getAcceptedLanguage($languages = null, $threshold = 0.1)
{
$accepts = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT_LANGUAGE'));
if ($languages === null) {
$accepts = array_keys($accepts);
return array_shift($accepts);
}
$top = 0;
$acceptedLanguage = false;
foreach ((array) $languages as $language) {
$search = array($language);
if ($identifiers = \Txp::get('\Textpattern\L10n\Locale')->getLocaleIdentifiers($language)) {
$search = array_map('strtolower', array_merge($search, $identifiers));
}
foreach ($accepts as $accept => $params) {
if (in_array(strtolower($accept), $search, true) && $params['q'] >= $threshold && $params['q'] >= $top) {
$top = $quality; // FIXME: $quality is made out of thin air.
$acceptedLanguage = $language;
}
}
}
return $acceptedLanguage;
}
/**
* Gets accepted encoding.
*
* Negotiates a common encoding between the client and the server.
*
*
* if (Txp::get('\Textpattern\Http\Request')->getAcceptedEncoding('gzip')) {
* echo 'Client accepts gzip.';
* }
*
*
* @param string|array $encodings Encoding
* @param float $threshold Quality threshold
* @return string|bool Encoding method, or FALSE
*/
public function getAcceptedEncoding($encodings = null, $threshold = 0.1)
{
$accepts = $this->getAcceptsMap($this->request->getVariable('HTTP_ACCEPT_ENCODING'));
if ($encodings === null) {
$accepts = array_keys($accepts);
return array_shift($accepts);
}
foreach ((array) $encodings as $encoding) {
if (isset($accepts[$encoding]) && $accepts[$encoding]['q'] >= $threshold) {
return $encoding;
}
}
return false;
}
/**
* Gets an absolute URL pointing to the requested document.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getUrl();
*
*
* The above will return URL pointing to the requested
* page, e.g. http://example.test/path/to/subpage.
*
* @return string The URL
*/
public function getUrl()
{
$port = '';
if (($portNumber = $this->getPort()) !== false && strpos($this->getHost(), ':') === false) {
$port = ':'.$portNumber;
}
return $this->getProtocol().'://'.$this->getHost().$port.$this->getUri();
}
/**
* Gets the server hostname.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getHost();
*
*
* Returns 'example.com' if requesting
* http://example.test/path/to/subpage.
*
* @return string The host
*/
public function getHost()
{
return (string) $this->request->getVariable('HTTP_HOST');
}
/**
* Gets the port, if not default.
*
* This method returns FALSE, if the port is the request protocol's default.
* Neither '80' or 443 for HTTPS are returned.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getPort();
*
*
* Returns '8080' if requesting http://example.test:8080/path/to/subpage.
*
* @return int|bool Port number, or FALSE
*/
public function getPort()
{
$port = (int) $this->request->getVariable('SERVER_PORT');
$protocol = $this->getProtocol();
if ($port && (!isset($this->protocolMap[$protocol]) || $port !== $this->protocolMap[$protocol])) {
return $port;
}
return false;
}
/**
* Gets the client IP address.
*
* This method supports proxies and uses 'X_FORWARDED_FOR' HTTP header if
* deemed necessary.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getIp();
*
*
* Returns the IP address the request came from, e.g. '0.0.0.0'.
* Can be either IPv6 or IPv4 depending on the request.
*
* @return string The IP address
*/
public function getIp()
{
$ip = $this->request->getVariable('REMOTE_ADDR');
$proxy = $this->getHeader('X-Forwarded-For');
if ($proxy && ($ip === '127.0.0.1' || $ip === '::1' || $ip === '::ffff:127.0.0.1' || $ip === $this->request->getVariable('SERVER_ADDR'))) {
$ips = explode(',', $proxy);
$ip = trim($ips[0]);
}
return $ip;
}
/**
* Gets client hostname.
*
* This method resolves client's hostname. It uses Textpattern's visitor
* logs as a cache layer.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getRemoteHostname();
*
*
* @return string|bool The hostname, or FALSE on failure
*/
public function getRemoteHostname()
{
$ip = $this->getIp();
if (($host = safe_field("host", 'txp_log', "ip = '".doSlash($ip)."' LIMIT 1")) !== false) {
return $host;
}
if ($host = @gethostbyaddr($ip)) {
if ($host !== $ip && @gethostbyname($host) !== $ip) {
return $ip;
}
return $host;
}
return false;
}
/**
* Gets the request protocol.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getProtocol();
*
*
* Returns 'https' if requesting https://example.test:8080/path/to/subpage.
*
* @return string Either 'http' or 'https'
*/
public function getProtocol()
{
if (($https = $this->request->getVariable('HTTPS')) && $https !== 'off') {
return 'https';
}
if (($https = $this->getHeader('Front-End-Https')) && strtolower($https) === 'on') {
return 'https';
}
if (($https = $this->getHeader('X-Forwarded-Proto')) && strtolower($https) === 'https') {
return 'https';
}
return 'http';
}
/**
* Gets referer.
*
* Returns referer header if it does not originate from the current
* hostname or come from a HTTPS page to a HTTP page.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getReferer();
*
*
* Returns full URL such as 'http://example.com/referring/page.php?id=12'.
*
* @return string|bool Referer, or FALSE if not available
*/
public function getReferer()
{
if ($this->referer === null) {
$protocol = $this->referer = false;
if ($referer = $this->request->getVariable('HTTP_REFERER')) {
if (strpos($referer, '://')) {
$referer = explode('://', $referer);
$protocol = array_shift($referer);
$referer = join('://', $referer);
}
if (!$protocol || ($protocol === 'https' && $this->getProtocol() !== 'https://')) {
return false;
}
if (preg_match('/^[^\.]*\.?'.preg_quote(preg_replace('/^www\./', '', $this->getHost()), '/').'/i', $referer)) {
return false;
}
$this->referer = $protocol.'://'.$referer;
}
}
return $this->referer;
}
/**
* Gets requested URI.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getUri();
*
*
* Returns '/some/requested/page?and=query' if requesting
* http://example.com/some/requested/page?and=query.
*
* @return string The URI
*/
public function getUri()
{
return (string) $this->request->getVariable('REQUEST_URI');
}
/**
* Gets an array map of raw request headers.
*
* This method is web server agnostic.
*
* The following:
*
*
* print_r(Txp::get('\Textpattern\Http\Request')->getHeaders());
*
*
* Returns:
*
*
* Array
* (
* [Host] => example.test
* [Connection] => keep-alive
* [Cache-Control] => max-age=0
* [User-Agent] => User-Agent
* [Referer] => http://example.test/textpattern/index.php
* [Accept-Encoding] => gzip,deflate,sdch
* [Accept-Language] => en-US,en;q=0.8,fi;q=0.6
* [Cookie] => toggle_show_spam=1
* )
*
*
* @return array An array of HTTP request headers
*/
public function getHeaders()
{
if ($this->headers !== null) {
return $this->headers;
}
if (function_exists('apache_request_headers')) {
if ($this->headers = apache_request_headers()) {
return $this->headers;
}
}
$this->headers = array();
foreach ($_SERVER as $name => $value) {
if (strpos($name, 'HTTP_') === 0 && is_scalar($value)) {
$parts = explode('_', $name);
array_shift($parts);
foreach ($parts as &$part) {
$part = ucfirst(strtolower($part));
}
$this->headers[join('-', $parts)] = (string) $value;
}
}
return $this->headers;
}
/**
* Gets a raw HTTP request header value.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getHeader('User-Agent');
*
*
* Will return the client's User-Agent header, if it has any. If the client
* didn't send User-Agent, the method returns FALSE.
*
* @param string $name The header name
* @return string|bool The header value, or FALSE on failure
*/
public function getHeader($name)
{
if ($headers = $this->getHeaders()) {
if (isset($headers[$name])) {
return $headers[$name];
}
}
return false;
}
/**
* Gets an array of HTTP cookies.
*
*
* print_r(Txp::get('\Textpattern\Http\Request')->getHeaders());
*
*
* Returns:
*
*
* Array(
* [foobar] => value
* )
*
*
* Returned cookie values are processed properly for you, and will not
* contain runtime quoting slashes or be URL encoded. Just pick and choose.
*
* @return array An array of cookies
*/
public function getCookies()
{
$out = array();
if ($_COOKIE) {
foreach ($_COOKIE as $name => $value) {
$out[$name] = $this->getCookie($name);
}
}
return $out;
}
/**
* Gets a HTTP cookie.
*
*
* echo Txp::get('\Textpattern\Http\Request')->getCookie('foobar');
*
*
* @param string $name The cookie name
* @return string The value
*/
public function getCookie($name)
{
if (isset($_COOKIE[$name])) {
if ($this->magicQuotesGpc) {
return doStrip($_COOKIE[$name]);
}
return $_COOKIE[$name];
}
return '';
}
/**
* Gets a query string.
*
*
* print_r(Txp::get('\Textpattern\Http\Request')->getQuery());
*
*
* If requesting "?event=article&step=save", the above returns:
*
*
* Array
* (
* [event] => article
* [step] => save
* )
*
*
* @return array An array of parameters
*/
public function getQuery()
{
$out = array();
if ($_GET) {
foreach ($_GET as $name => $value) {
$out[$name] = $this->getParam($name);
}
}
if ($_POST) {
foreach ($_POST as $name => $value) {
$out[$name] = $this->getPost($name);
}
}
return $out;
}
/**
* Gets a HTTP query string parameter.
*
* @param $name The parameter name
* @return mixed
*/
public function getParam($name)
{
if (isset($_GET[$name])) {
$out = $_GET[$name];
if ($this->magicQuotesGpc) {
$out = doStrip($out);
}
$out = doArray($out, 'deCRLF');
return doArray($out, 'deNull');
}
return $this->getPost($name);
}
/**
* Gets a HTTP post parameter.
*
* @param string $name The parameter name
* @return mixed
*/
public function getPost($name)
{
$out = '';
if (isset($_POST[$name])) {
$out = $_POST[$name];
if ($this->magicQuotesGpc) {
$out = doStrip($out);
}
}
return doArray($out, 'deNull');
}
/**
* Builds a content-negotiation accepts map from the given value.
*
* Keys are the accepted type and the value are the params. If client
* doesn't specify quality, defaults to 1.0. Values are sorted by the
* quality, from the highest to the lowest.
*
* This method can be used to parse Accept, Accept-Charset, Accept-Encoding
* and Accept-Language header values.
*
*
* print_r(Txp::get('\Textpattern\Http\Request')->getAcceptsMap('en-us;q=1.0,en;q=0.9'));
*
*
* Returns:
*
*
* Array
* (
* [en-us] => Array
* (
* [q] => 1.0
* )
* [en] => Array
* (
* [q] => 0.9
* )
* )
*
*
* @param string $header The header string
* @return array Accepts map
*/
public function getAcceptsMap($header)
{
$types = explode(',', $header);
$accepts = array();
$sort = array();
foreach ($types as $type) {
if ($type = trim($type)) {
if ($parts = explode(';', $type)) {
$type = array_shift($parts);
$params = array(
'q' => 1.0,
);
foreach ($parts as $value) {
if (strpos($value, '=') === false) {
$params[$value] = true;
} else {
$value = explode('=', $value);
$params[array_shift($value)] = join('=', $value);
}
}
$params['q'] = floatval($params['q']);
$accepts[$type] = $params;
$sort[$type] = $params['q'];
}
}
}
array_multisort($sort, SORT_DESC, $accepts);
return $accepts;
}
}