. */ /** * 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; } }