<?php
namespace XF\Http;
use function array_key_exists, count, floatval, in_array, intval, is_array, is_string, strlen, strval;
class Request
{
/**
* @var \XF\InputFilterer
*/
protected $filterer;
protected $input;
protected $files;
protected $cookie;
protected $server;
protected $skipLogKeys = ['_xfToken'];
protected $cookiePrefix = '';
public static $googleIps = [
'v4' => [
'35.190.247.0/24',
'35.191.0.0/16',
'64.233.160.0/19',
'66.102.0.0/20',
'66.249.80.0/20',
'72.14.192.0/18',
'74.125.0.0/16',
'108.177.8.0/21',
'108.177.96.0/19',
'130.211.0.0/22',
'172.217.0.0/19',
'172.217.32.0/20',
'172.217.128.0/19',
'172.217.160.0/20',
'172.217.192.0/19',
'172.253.56.0/21',
'172.253.112.0/20',
'173.194.0.0/16',
'209.85.128.0/17',
'216.58.192.0/19',
'216.239.32.0/19'
],
'v6' => [
'2a00:1450:4000::/36',
'2c0f:fb50:4000::/36',
'2001:4860:4000::/36',
'2404:6800:4000::/36',
'2607:f8b0:4000::/36',
'2800:3f0:4000::/36'
]
];
public static $googleCloudIps = [
'v4' => [
'8.34.208.0/23',
'8.34.210.0/24',
'8.34.211.0/24',
'8.34.212.0/22',
'8.34.216.0/22',
'8.34.220.0/22',
'8.35.192.0/21',
'23.236.48.0/20',
'23.251.128.0/20',
'23.251.144.0/20',
'34.64.64.0/22',
'34.64.68.0/22',
'34.64.72.0/21',
'34.64.80.0/20',
'34.64.96.0/19',
'34.64.128.0/22',
'34.64.132.0/22',
'34.64.136.0/21',
'34.64.144.0/20',
'34.64.160.0/19',
'34.64.192.0/18',
'34.65.0.0/16',
'34.66.0.0/15',
'34.68.0.0/14',
'34.72.0.0/16',
'34.73.0.0/16',
'34.74.0.0/15',
'34.76.0.0/14',
'34.80.0.0/15',
'34.82.0.0/15',
'34.84.0.0/16',
'34.85.0.0/17',
'34.85.128.0/17',
'34.86.0.0/16',
'34.87.0.0/17',
'34.87.128.0/18',
'34.87.192.0/18',
'34.88.0.0/16',
'34.89.0.0/17',
'34.89.128.0/17',
'34.90.0.0/15',
'34.92.0.0/16',
'34.93.0.0/16',
'34.94.0.0/16',
'34.95.0.0/18',
'34.95.64.0/18',
'34.95.128.0/17',
'34.96.64.0/18',
'34.96.128.0/17',
'34.97.0.0/16',
'34.98.64.0/18',
'34.98.128.0/21',
'34.100.128.0/17',
'34.101.18.0/24',
'34.101.20.0/22',
'34.101.24.0/22',
'34.101.64.0/18',
'34.101.128.0/17',
'34.102.0.0/17',
'34.102.128.0/17',
'34.104.27.0/24',
'34.104.49.0/24',
'34.104.50.0/23',
'34.104.52.0/24',
'34.104.58.0/23',
'34.104.60.0/23',
'34.104.62.0/23',
'34.104.64.0/21',
'34.104.72.0/22',
'34.104.76.0/22',
'34.104.80.0/21',
'34.104.88.0/21',
'34.104.96.0/21',
'34.104.104.0/23',
'34.104.106.0/23',
'34.104.108.0/23',
'34.104.110.0/23',
'34.104.112.0/23',
'34.104.114.0/23',
'34.104.116.0/22',
'34.104.120.0/23',
'34.104.122.0/23',
'34.104.124.0/23',
'34.104.126.0/23',
'34.104.128.0/17',
'34.105.0.0/17',
'34.105.128.0/17',
'34.106.0.0/16',
'34.107.0.0/17',
'34.107.128.0/17',
'34.108.0.0/16',
'34.110.8.0/24',
'34.110.128.0/17',
'34.111.0.0/16',
'34.116.0.0/21',
'34.116.64.0/18',
'34.116.128.0/17',
'34.117.0.0/16',
'34.118.0.0/17',
'34.120.0.0/16',
'34.121.0.0/16',
'34.122.0.0/15',
'34.124.0.0/21',
'34.124.8.0/22',
'34.124.12.0/22',
'34.124.16.0/21',
'34.124.24.0/21',
'34.124.32.0/21',
'34.124.40.0/23',
'34.124.42.0/23',
'34.124.44.0/23',
'34.124.46.0/23',
'34.124.48.0/23',
'34.124.50.0/23',
'34.124.52.0/22',
'34.124.56.0/23',
'34.124.58.0/23',
'34.124.60.0/23',
'34.124.62.0/23',
'34.124.112.0/20',
'34.124.128.0/17',
'34.125.0.0/16',
'34.126.64.0/18',
'34.126.128.0/18',
'34.126.192.0/20',
'34.126.208.0/20',
'34.127.0.0/17',
'34.127.177.0/24',
'34.127.178.0/23',
'34.127.180.0/24',
'34.127.186.0/23',
'34.127.188.0/23',
'34.127.190.0/23',
'34.129.0.0/16',
'34.130.0.0/16',
'34.131.0.0/16',
'34.132.0.0/14',
'34.136.0.0/16',
'34.137.0.0/16',
'34.138.0.0/15',
'34.140.0.0/16',
'34.141.0.0/17',
'34.141.128.0/17',
'34.142.0.0/17',
'34.145.0.0/17',
'34.145.128.0/17',
'34.146.0.0/16',
'34.147.0.0/17',
'34.147.128.0/17',
'34.148.0.0/16',
'34.149.0.0/16',
'34.150.0.0/17',
'34.150.128.0/17',
'34.151.0.0/18',
'34.151.64.0/18',
'34.151.128.0/18',
'34.151.192.0/18',
'34.152.0.0/18',
'34.157.0.0/21',
'34.157.16.0/20',
'34.157.36.0/22',
'34.157.40.0/22',
'34.157.48.0/20',
'34.157.64.0/20',
'34.157.128.0/21',
'34.157.144.0/20',
'34.157.164.0/22',
'34.157.168.0/22',
'34.157.176.0/20',
'34.157.192.0/20',
'34.159.0.0/16',
'34.168.0.0/15',
'34.170.0.0/15',
'34.172.0.0/15',
'34.176.0.0/16',
'35.184.0.0/16',
'35.185.0.0/17',
'35.185.128.0/19',
'35.185.160.0/20',
'35.185.176.0/20',
'35.185.192.0/18',
'35.186.0.0/17',
'35.186.128.0/20',
'35.186.144.0/20',
'35.186.160.0/19',
'35.186.192.0/18',
'35.187.0.0/17',
'35.187.144.0/20',
'35.187.160.0/19',
'35.187.192.0/19',
'35.187.224.0/19',
'35.188.0.0/17',
'35.188.128.0/18',
'35.188.192.0/19',
'35.188.224.0/19',
'35.189.0.0/18',
'35.189.64.0/18',
'35.189.128.0/19',
'35.189.160.0/19',
'35.189.192.0/18',
'35.190.0.0/18',
'35.190.64.0/19',
'35.190.112.0/20',
'35.190.128.0/18',
'35.190.192.0/19',
'35.190.224.0/20',
'35.192.0.0/15',
'35.194.0.0/18',
'35.194.64.0/19',
'35.194.96.0/19',
'35.194.128.0/17',
'35.195.0.0/16',
'35.196.0.0/16',
'35.197.0.0/17',
'35.197.128.0/19',
'35.197.160.0/19',
'35.197.192.0/18',
'35.198.0.0/18',
'35.198.64.0/18',
'35.198.128.0/18',
'35.198.192.0/18',
'35.199.0.0/18',
'35.199.64.0/18',
'35.199.144.0/20',
'35.199.160.0/19',
'35.200.0.0/17',
'35.200.128.0/17',
'35.201.0.0/19',
'35.201.41.0/24',
'35.201.64.0/18',
'35.201.128.0/17',
'35.202.0.0/16',
'35.203.0.0/17',
'35.203.128.0/18',
'35.203.210.0/23',
'35.203.212.0/22',
'35.203.216.0/22',
'35.203.232.0/21',
'35.204.0.0/16',
'35.205.0.0/16',
'35.206.32.0/19',
'35.206.64.0/18',
'35.206.128.0/18',
'35.206.192.0/18',
'35.207.0.0/18',
'35.207.64.0/18',
'35.207.128.0/18',
'35.207.192.0/18',
'35.208.0.0/15',
'35.210.0.0/16',
'35.211.0.0/16',
'35.212.0.0/17',
'35.212.128.0/17',
'35.213.0.0/17',
'35.213.128.0/18',
'35.213.192.0/18',
'35.214.0.0/17',
'35.214.128.0/17',
'35.215.0.0/18',
'35.215.64.0/18',
'35.215.128.0/18',
'35.215.192.0/18',
'35.216.0.0/17',
'35.216.128.0/17',
'35.217.0.0/18',
'35.217.64.0/18',
'35.217.128.0/17',
'35.219.0.0/17',
'35.219.128.0/18',
'35.220.0.0/20',
'35.220.16.0/23',
'35.220.18.0/23',
'35.220.20.0/22',
'35.220.24.0/23',
'35.220.26.0/24',
'35.220.27.0/24',
'35.220.31.0/24',
'35.220.32.0/21',
'35.220.40.0/24',
'35.220.41.0/24',
'35.220.42.0/24',
'35.220.43.0/24',
'35.220.44.0/24',
'35.220.45.0/24',
'35.220.46.0/24',
'35.220.47.0/24',
'35.220.48.0/21',
'35.220.56.0/22',
'35.220.60.0/22',
'35.220.64.0/19',
'35.220.96.0/19',
'35.220.128.0/17',
'35.221.0.0/18',
'35.221.64.0/18',
'35.221.128.0/17',
'35.222.0.0/15',
'35.224.0.0/15',
'35.226.0.0/16',
'35.227.0.0/17',
'35.227.128.0/18',
'35.227.192.0/18',
'35.228.0.0/16',
'35.229.16.0/20',
'35.229.32.0/19',
'35.229.64.0/18',
'35.229.128.0/17',
'35.230.0.0/17',
'35.230.128.0/19',
'35.230.160.0/19',
'35.230.240.0/20',
'35.231.0.0/16',
'35.232.0.0/16',
'35.233.0.0/17',
'35.233.128.0/17',
'35.234.0.0/18',
'35.234.64.0/18',
'35.234.128.0/19',
'35.234.160.0/20',
'35.234.176.0/20',
'35.234.192.0/20',
'35.234.208.0/20',
'35.234.224.0/20',
'35.234.240.0/20',
'35.235.0.0/20',
'35.235.16.0/20',
'35.235.32.0/20',
'35.235.48.0/20',
'35.235.64.0/18',
'35.235.216.0/21',
'35.236.0.0/17',
'35.236.128.0/18',
'35.236.192.0/18',
'35.237.0.0/16',
'35.238.0.0/15',
'35.240.0.0/17',
'35.240.128.0/17',
'35.241.0.0/18',
'35.241.64.0/18',
'35.241.128.0/17',
'35.242.0.0/20',
'35.242.16.0/23',
'35.242.18.0/23',
'35.242.20.0/22',
'35.242.24.0/23',
'35.242.26.0/24',
'35.242.27.0/24',
'35.242.31.0/24',
'35.242.32.0/21',
'35.242.40.0/24',
'35.242.41.0/24',
'35.242.42.0/24',
'35.242.43.0/24',
'35.242.44.0/24',
'35.242.45.0/24',
'35.242.46.0/24',
'35.242.47.0/24',
'35.242.48.0/21',
'35.242.56.0/22',
'35.242.60.0/22',
'35.242.64.0/19',
'35.242.96.0/19',
'35.242.128.0/18',
'35.242.192.0/18',
'35.243.0.0/21',
'35.243.8.0/21',
'35.243.32.0/21',
'35.243.40.0/21',
'35.243.56.0/21',
'35.243.64.0/18',
'35.243.128.0/17',
'35.244.0.0/18',
'35.244.64.0/18',
'35.244.128.0/17',
'35.245.0.0/16',
'35.246.0.0/17',
'35.246.128.0/17',
'35.247.0.0/17',
'35.247.128.0/18',
'35.247.192.0/18',
'104.154.16.0/20',
'104.154.32.0/19',
'104.154.64.0/19',
'104.154.96.0/20',
'104.154.113.0/24',
'104.154.114.0/23',
'104.154.116.0/22',
'104.154.120.0/23',
'104.154.128.0/17',
'104.155.0.0/17',
'104.155.128.0/18',
'104.155.192.0/19',
'104.155.224.0/20',
'104.196.0.0/18',
'104.196.65.0/24',
'104.196.66.0/23',
'104.196.68.0/22',
'104.196.96.0/19',
'104.196.128.0/18',
'104.196.192.0/19',
'104.196.224.0/19',
'104.197.0.0/16',
'104.198.0.0/20',
'104.198.16.0/20',
'104.198.32.0/19',
'104.198.64.0/20',
'104.198.80.0/20',
'104.198.96.0/20',
'104.198.112.0/20',
'104.198.128.0/17',
'104.199.0.0/18',
'104.199.66.0/23',
'104.199.68.0/22',
'104.199.72.0/21',
'104.199.80.0/20',
'104.199.96.0/20',
'104.199.112.0/20',
'104.199.128.0/18',
'104.199.192.0/19',
'104.199.224.0/20',
'104.199.242.0/23',
'104.199.244.0/22',
'104.199.248.0/21',
'107.167.160.0/20',
'107.167.176.0/20',
'107.178.208.0/20',
'107.178.240.0/20',
'108.59.80.0/21',
'108.59.88.0/21',
'130.211.4.0/22',
'130.211.8.0/21',
'130.211.16.0/20',
'130.211.32.0/20',
'130.211.48.0/20',
'130.211.64.0/19',
'130.211.96.0/20',
'130.211.112.0/20',
'130.211.128.0/18',
'130.211.192.0/19',
'130.211.224.0/20',
'130.211.240.0/20',
'146.148.2.0/23',
'146.148.4.0/22',
'146.148.8.0/21',
'146.148.16.0/20',
'146.148.32.0/19',
'146.148.64.0/19',
'146.148.96.0/20',
'146.148.112.0/20',
'162.216.148.0/22',
'162.222.176.0/21',
'173.255.112.0/21',
'173.255.120.0/21',
'192.158.28.0/22',
'199.192.115.0/24',
'199.223.232.0/22',
'199.223.236.0/24'
],
'v6' => [
'2600:1901:1:1000::/52',
'2600:1901:1:2000::/51',
'2600:1901:1:4000::/50',
'2600:1901:1:8000::/49',
'2600:1901::/48'
],
];
public static $cloudFlareIps = [
'v4' => [
'103.21.244.0/22',
'103.22.200.0/22',
'103.31.4.0/22',
'104.16.0.0/13',
'104.24.0.0/14',
'108.162.192.0/18',
'131.0.72.0/22',
'141.101.64.0/18',
'162.158.0.0/15',
'172.64.0.0/13',
'173.245.48.0/20',
'188.114.96.0/20',
'190.93.240.0/20',
'197.234.240.0/22',
'198.41.128.0/17'
],
'v6' => [
'2a06:98c0::/29',
'2c0f:f248::/32',
'2400:cb00::/32',
'2405:8100::/32',
'2405:b500::/32',
'2606:4700::/32',
'2803:f800::/32'
]
];
protected $remoteIp = null;
protected $robotName;
protected $fromSearch;
protected static $customMethodPhpInput = null;
public function __construct(\XF\InputFilterer $filterer,
array $input = null, array $files = null, array $cookie = null, array $server = null
)
{
$this->filterer = $filterer;
if ($input === null)
{
if (self::$customMethodPhpInput === null)
{
self::$customMethodPhpInput = $this->convertCustomMethodPhpInput();
}
$input = self::$customMethodPhpInput + $_POST + $_GET;
}
if ($files === null)
{
$files = $_FILES;
}
if ($cookie === null)
{
$cookie = $_COOKIE;
}
if ($server === null)
{
$server = $_SERVER;
}
$this->input = $input;
$this->files = $files;
$this->cookie = $cookie;
$this->server = $server;
}
protected function convertCustomMethodPhpInput()
{
if (!empty($_SERVER['REQUEST_METHOD'])
&& in_array(strtoupper($_SERVER['REQUEST_METHOD']), ['PUT', 'PATCH', 'DELETE'])
&& !empty($_SERVER['CONTENT_TYPE'])
&& $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'
)
{
$rawInput = @file_get_contents("php://input");
if ($rawInput)
{
parse_str($rawInput, $extra);
if (is_array($extra))
{
return $extra;
}
}
}
return [];
}
public function setCookiePrefix($prefix)
{
$this->cookiePrefix = $prefix;
}
public function getCookiePrefix()
{
return $this->cookiePrefix;
}
public function get($key, $fallback = false)
{
$subParts = explode('.', $key);
$key = array_shift($subParts);
if (array_key_exists($key, $this->input))
{
$value = $this->input[$key];
}
else
{
return $fallback;
}
return $this->getSubValue($value, $subParts, $fallback);
}
public function exists($key)
{
$subParts = explode('.', $key);
$key = array_shift($subParts);
if (array_key_exists($key, $this->input))
{
$value = $this->input[$key];
}
else
{
return false;
}
while ($subParts)
{
if (!is_array($value))
{
return false;
}
$key = array_shift($subParts);
if (array_key_exists($key, $value))
{
$value = $value[$key];
}
else
{
return false;
}
}
return true;
}
public function getUser($key, $fallback = false)
{
return $this->get($key, $fallback);
}
protected function getSubValue($value, array $subParts, $fallback)
{
while ($subParts)
{
if (!is_array($value))
{
return $fallback;
}
$key = array_shift($subParts);
if (array_key_exists($key, $value))
{
$value = $value[$key];
}
else
{
return $fallback;
}
}
return $value;
}
public function filter($key, $type = null, $default = null)
{
if (is_array($key) && $type === null)
{
$output = [];
foreach ($key AS $name => $value)
{
if (is_array($value))
{
$array = $this->get($name);
if (!is_array($array))
{
$array = [];
}
$output[$name] = $this->filterer->filterArray($array, $value);
}
else
{
$output[$name] = $this->filter($name, $value);
}
}
return $output;
}
else
{
$value = $this->get($key, $default);
if (is_string($type) && $type[0] == '?')
{
if ($value === null)
{
return null;
}
$type = substr($type, 1);
}
if (is_array($type))
{
if (!is_array($value))
{
$value = [];
}
return $this->filterer->filterArray($value, $type);
}
else
{
return $this->filterer->filter($value, $type);
}
}
}
/**
* @param $key string Input key to set - either 'keyName' or 'arrayName.subArrayName.keyName' etc.
* @param $value
*/
public function set($key, $value)
{
$parts = explode('.', $key);
$var =& $this->input;
while ($part = array_shift($parts))
{
$var =& $var[$part];
}
$var = $value;
}
public function getInput()
{
return $this->input;
}
public function getInputForLogs()
{
return $this->filterForLog($this->input);
}
public function filterForLog(array $data)
{
$skip = array_fill_keys($this->skipLogKeys, true);
$filter = function(array $d) use ($skip, &$filter)
{
$output = [];
foreach ($d AS $k => $v)
{
if (isset($skip[$k]) || strpos($k, 'password') !== false)
{
$output[$k] = '********';
}
else if (is_array($v))
{
$output[$k] = $filter($v);
}
else
{
$output[$k] = $v;
}
}
return $output;
};
return $filter($data);
}
public function skipKeyForLogging($key)
{
$this->skipLogKeys[] = $key;
}
public function fileExists($key)
{
return isset($this->files[$key]['name']);
}
/**
* @param string $key
* @param bool $multiple If true, returns an array of uploads for this key
* @param bool $skipErrors If true, uploads with errors will not be returned
*
* @return Upload|Upload[]
*/
public function getFile($key, $multiple = false, $skipErrors = true)
{
if (!$this->fileExists($key))
{
return ($multiple ? [] : null);
}
if (is_array($this->files[$key]['name']))
{
// multiple uploads
$files = [];
foreach (array_keys($this->files[$key]['name']) AS $idx)
{
$files[$idx] = [
'name' => $this->files[$key]['name'][$idx],
'type' => $this->files[$key]['type'][$idx],
'size' => $this->files[$key]['size'][$idx],
'tmp_name' => $this->files[$key]['tmp_name'][$idx],
'error' => $this->files[$key]['error'][$idx],
];
}
}
else
{
// single upload
$files = [$this->files[$key]];
}
$output = [];
$imageI = 1;
$imageBase = 'img-' . gmdate('Y-m-d-H-i-s') . '-';
foreach ($files AS $idx => $file)
{
if ($file['error'] == UPLOAD_ERR_NO_FILE || ($skipErrors && $file['error']))
{
// didn't upload a file or has errors - just ignore
continue;
}
// this handles files uploaded via JS that don't have a proper filename
if ($file['name'] == 'blob' && preg_match('#^image/(pjpeg|jpeg|gif|png)$#', $file['type'], $match))
{
switch ($match[1])
{
case 'jpeg':
case 'pjpeg':
$type = 'jpg';
break;
default:
$type = $match[1];
}
$file['name'] = $imageBase . $imageI . '.' . $type;
$imageI++;
}
$class = \XF::extendClass('XF\Http\Upload');
$output[$idx] = new $class($file['tmp_name'], $file['name'], $file['error']);
}
if ($multiple)
{
return $output;
}
else
{
return reset($output);
}
}
public function getCookie($key, $fallback = false)
{
$cookie = $this->getCookieRaw($this->cookiePrefix . $key, $fallback);
if (is_array($cookie) && !is_array($fallback))
{
$cookie = $fallback;
}
return $cookie;
}
public function getCookieArray($key, array $fallback = [])
{
$cookie = $this->getCookieRaw($this->cookiePrefix . $key, $fallback);
if (!is_array($cookie))
{
$cookie = $fallback;
}
return $cookie;
}
public function getCookies($prefixFiltered = true)
{
if (!$prefixFiltered)
{
return $this->cookie;
}
$output = [];
$prefixLength = strlen($this->cookiePrefix);
foreach ($this->cookie AS $cookie => $value)
{
if (substr($cookie, 0, $prefixLength) == $this->cookiePrefix)
{
$cookie = substr($cookie, $prefixLength);
if (is_string($cookie) && strlen($cookie))
{
$output[$cookie] = $value;
}
}
}
return $output;
}
public function getCookieRaw($key, $fallback = false)
{
if (array_key_exists($key, $this->cookie))
{
return $this->cookie[$key];
}
else
{
return $fallback;
}
}
public function getInputRaw($fallback = '')
{
$input = file_get_contents('php://input');
return ($input ?: $fallback);
}
public function getIp($allowProxied = false)
{
if ($allowProxied && $ip = $this->getServer('HTTP_CLIENT_IP'))
{
list($ip) = explode(',', $ip);
return $this->getFilteredIp($ip);
}
else if ($allowProxied && $ip = $this->getServer('HTTP_X_FORWARDED_FOR'))
{
list($ip) = explode(',', $ip);
return $this->getFilteredIp($ip);
}
if ($this->remoteIp === null)
{
$ip = $this->getTrustedRealIp($this->getServer('REMOTE_ADDR'));
$this->remoteIp = $this->getFilteredIp($ip);
}
return $this->remoteIp;
}
public function getAllIps()
{
$proxied = $this->getIp(true);
$unproxied = $this->getIp(false);
if ($proxied === $unproxied)
{
return $unproxied;
}
$ips = preg_split('/,\s*/', $proxied);
$ips[] = $unproxied;
return array_unique($ips);
}
protected function getTrustedRealIp($ip)
{
$via = $this->getServer('HTTP_VIA');
if ($via && strpos(strtolower($via), 'chrome-compression-proxy'))
{
// may have Google Data Saver enabled
$realIps = $this->getServer('HTTP_X_FORWARDED_FOR');
if ($realIps)
{
$realIps = explode(',', $realIps);
$realIp = end($realIps);
$realIp = trim($realIp);
if ($this->ipMatchesRanges($ip, self::$googleIps))
{
if (!$this->ipMatchesRanges($ip, self::$googleCloudIps))
{
// if the IP comes from a known Google IP, but NOT listed as a known Google Cloud IP
// then we can trust that they put the client IP in X-Forwarded-For
// (They should have appended it to the end.)
return $realIp;
}
}
}
}
$cfIp = $this->getServer('HTTP_CF_CONNECTING_IP');
if ($cfIp && $cfIp !== $ip)
{
if ($this->ipMatchesRanges($ip, self::$cloudFlareIps))
{
// connection from known CloudFlare IP, real IP in their header
return $cfIp;
}
}
return $ip;
}
protected function ipMatchesRanges($ip, array $ranges)
{
$ip = \XF\Util\Ip::convertIpStringToBinary($ip);
if ($ip === false)
{
return false;
}
$type = strlen($ip) == 4 ? 'v4' : 'v6';
if (empty($ranges[$type]))
{
return false;
}
foreach ($ranges[$type] AS $range)
{
if (is_string($range))
{
$range = explode('/', $range);
}
$rangeIp = \XF\Util\Ip::convertIpStringToBinary($range[0]);
$cidr = intval($range[1]);
if (\XF\Util\Ip::ipMatchesCidrRange($ip, $rangeIp, $cidr))
{
return true;
}
}
return false;
}
protected function getFilteredIp($ip)
{
$ip = trim($ip);
if (preg_match('#:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$#', $ip, $match))
{
// embedded IPv4
$long = ip2long($match[1]);
if (!$long)
{
return $ip;
}
return $match[1];
}
return $ip;
}
public function getUserAgent()
{
return $this->getServer('HTTP_USER_AGENT');
}
public function getBrowser(): array
{
$ua = strtolower($this->getUserAgent());
$match = [];
$browser = [];
preg_match('/trident\/.*rv:([0-9.]+)/', $ua, $match);
if ($match)
{
$browser = [
'browser' => 'msie',
'version' => floatval($match[1])
];
}
else
{
if (
!preg_match('/(msie)[ \/]([0-9\.]+)/', $ua, $match) &&
!preg_match('/(edge)[ \/]([0-9\.]+)/', $ua, $match) &&
!preg_match('/(chrome)[ \/]([0-9\.]+)/', $ua, $match) &&
!preg_match('/(webkit)[ \/]([0-9\.]+)/', $ua, $match) &&
!preg_match('/(opera)(?:.*version|)[ \/]([0-9\.]+)/', $ua, $match) &&
!(
strpos($ua, 'compatible') === false &&
preg_match('/(mozilla)(?:.*? rv:([0-9\.]+)|)/', $ua, $match)
)
)
{
$match = [];
}
if ($match && $match[1] == 'webkit' && strpos($ua, 'safari') !== false)
{
preg_match('/version[ \/]([0-9\.]+)/', $ua, $safariMatch);
if ($safariMatch)
{
$match = [$match[0], 'safari', $safariMatch[1]];
}
}
$browser = [
'browser' => $match[1] ?? '',
'version' => isset($match[2]) ? floatval($match[2]) : 0.0
];
}
$os = '';
$osVersion = null;
$osMatch = [];
if (preg_match('/(ipad|iphone|ipod)/', $ua))
{
$os = 'ios';
preg_match('/os ([0-9_]+)/', $ua, $osMatch);
if ($osMatch)
{
$osVersion = floatval(str_replace('_', '.', $osMatch[1]));
}
}
else if (preg_match('/android[ \/]([0-9\.]+)/', $ua, $osMatch))
{
$os = 'android';
$osVersion = floatval($osMatch[1]);
}
else if (preg_match('/windows /', $ua))
{
$os = 'windows';
}
else if (preg_match('/linux/', $ua))
{
$os = 'linux';
}
else if (preg_match('/mac os/', $ua))
{
$os = 'mac';
}
$browser['os'] = $os;
$browser['osVersion'] = $osVersion;
return $browser;
}
public function getRobotName()
{
if ($this->robotName === null)
{
$userAgent = $this->getUserAgent();
if ($userAgent)
{
$this->robotName = \XF::app()->data('XF:Robot')->userAgentMatchesRobot($userAgent);
}
else
{
$this->robotName = '';
}
}
return $this->robotName;
}
/**
* @return string
*/
public function getFromSearch()
{
if ($this->fromSearch === null)
{
$this->populateFromSearch();
}
return $this->fromSearch;
}
public function populateFromSearch(Response $persistResponse = null)
{
$fromSearch = $this->getCookie('from_search');
if (!is_string($fromSearch))
{
$referrer = $this->getReferrer();
if ($referrer)
{
$fromSearch = \XF::app()->data('XF:Search')->urlMatchesSearchDomain($referrer);
if ($persistResponse && $fromSearch)
{
$persistResponse->setCookie('from_search', $fromSearch, 0, null, false);
}
}
else
{
$fromSearch = '';
}
}
$this->fromSearch = $fromSearch;
return $this->fromSearch;
}
public function getReferrer()
{
$referrer = $this->getServer('HTTP_REFERER');
if ($referrer && strpos($referrer, 'service_worker.js') !== false)
{
// Safari might put the service worker in as the referrer, which is
// never correct
$referrer = false;
}
return $referrer;
}
public function getServer($key, $fallback = false)
{
if (array_key_exists($key, $this->server))
{
return $this->server[$key];
}
else
{
return $fallback;
}
}
public function getRequestMethod()
{
return strtolower($this->getServer('REQUEST_METHOD'));
}
public function getApiKey()
{
return trim($this->getServer('HTTP_XF_API_KEY', ''));
}
public function getApiUser()
{
return intval($this->getServer('HTTP_XF_API_USER', 0));
}
public function isGet()
{
return ($this->getRequestMethod() === 'get');
}
public function isHead()
{
return ($this->getRequestMethod() === 'head');
}
public function isPost()
{
return ($this->getRequestMethod() === 'post');
}
public function isXhr()
{
return ($this->getServer('HTTP_X_REQUESTED_WITH') === 'XMLHttpRequest');
}
public function isSecure()
{
return (
$this->getServer('REQUEST_SCHEME') === 'https'
|| $this->getServer('HTTP_X_FORWARDED_PROTO') === 'https'
|| $this->getServer('HTTPS') === 'on'
|| $this->getServer('SERVER_PORT') == 443
);
}
public function isPrefetch()
{
return (
$this->getServer('HTTP_X_MOZ') === 'prefetch'
|| $this->getServer('HTTP_X_PURPOSE') === 'prefetch'
|| $this->getServer('HTTP_PURPOSE') === 'prefetch'
);
}
/**
* Returns true if the host name (either provided or the current one) is a
* locally/loopback served page. Note that this should not be used in security-related
* constructs as it may rely on an unverified, user-provided value.
*
* This function's primary use is in conjunction with isSecure() checks, to allow access
* to certain client functionality that usually requires a secure connection but is allowed
* on local/loopback addresses.
*
* @param string|null $host Current host if not specified
*
* @return bool
*/
public function isHostLocal($host = null)
{
if ($host === null)
{
$host = $this->getHost();
}
return (
$host == 'localhost'
|| $host == '[::1]'
|| substr($host, -10) === '.localhost'
|| preg_match('#^127\.\d+\.\d+\.\d+$#', $host)
);
}
public function getProtocol()
{
return $this->isSecure() ? 'https' : 'http';
}
public function getBaseUrl()
{
$baseUrl = $this->getServer('SCRIPT_NAME', '');
$basePath = dirname($baseUrl);
if (strlen($basePath) <= 1)
{
// Looks to be at the root, so trust that.
return $baseUrl;
}
$requestUri = $this->getRequestUri();
if (!strlen($requestUri))
{
// no request URI, probably not a normal HTTP request - just return the root
return '/';
}
if (strpos($requestUri, $basePath) === 0)
{
// We're not at the root but we match the first part of the request URI, so trust that.
return $baseUrl;
}
// Otherwise, the SCRIPT_NAME is wrong and likely has extra stuff prepended. See if we can find the request
// URI in the base URL. If so, ignore what comes before it.
$qsPos = strpos($requestUri, '?');
if ($qsPos !== false)
{
$requestUriNoQs = substr($requestUri, 0, $qsPos);
}
else
{
$requestUriNoQs = $requestUri;
}
$requestPos = strpos($baseUrl, $requestUriNoQs);
if ($requestPos)
{
$realBaseUrl = substr($baseUrl, $requestPos);
if ($realBaseUrl)
{
return $realBaseUrl;
}
}
return $baseUrl;
}
public function getBasePath()
{
$baseUrl = $this->getBaseUrl();
if (is_string($baseUrl) && strlen($baseUrl))
{
$lastSlash = strrpos($baseUrl, '/');
if ($lastSlash) // intentionally skipping for false and 0
{
return substr($baseUrl, 0, $lastSlash);
}
}
return '/';
}
public function getFullBasePath()
{
return $this->getHostUrl() . $this->getBasePath();
}
public function getXfRootPath()
{
$basePath = $this->getBasePath();
$scriptPath = $this->getServer('SCRIPT_FILENAME', '');
if (!$scriptPath)
{
return $basePath;
}
$trailingPath = \XF\Util\File::stripRootPathPrefix($scriptPath);
if (!$trailingPath)
{
// stripping the root path failed, so we know the trailing path isn't correct
return $basePath;
}
$trailingPath = dirname($trailingPath);
if (\XF::$DS !== '/')
{
$trailingPath = str_replace(\XF::$DS, '/', $trailingPath);
}
if (!$trailingPath || $trailingPath === '/')
{
// the script we're running is in the XF root, just use the base path
return $basePath;
}
else if (substr($basePath, -strlen($trailingPath)) === $trailingPath)
{
$rootPath = substr($basePath, 0, -strlen($trailingPath));
if ($rootPath)
{
$rootPath = rtrim($rootPath, '/');
}
return $rootPath ?: '/';
}
else
{
return $basePath;
}
}
public function getFullXfRootPath()
{
return $this->getHostUrl() . $this->getXfRootPath();
}
public function getExtendedUrl($requestUri = null)
{
$baseUrl = $this->getBaseUrl();
$basePath = $this->getBasePath();
if ($requestUri === null)
{
$requestUri = $this->getRequestUri();
}
if (strpos($requestUri, $baseUrl) === 0)
{
return strval(substr($requestUri, strlen($baseUrl)));
}
else if (strpos($requestUri, $basePath) === 0)
{
return strval(substr($requestUri, strlen($basePath)));
}
else
{
return $requestUri;
}
}
public function getRequestUri()
{
if ($this->getServer('IIS_WasUrlRewritten') === '1')
{
$unencodedUrl = $this->getServer('UNENCODED_URL', '');
if ($unencodedUrl !== '')
{
return $unencodedUrl;
}
}
return $this->getServer('REQUEST_URI', '');
}
public function getFullRequestUri()
{
return $this->getHostUrl() . $this->getRequestUri();
}
public function getHost()
{
$host = $this->getServer('HTTP_HOST');
if (!$host)
{
$host = $this->getServer('SERVER_NAME');
$port = intval($this->getServer('SERVER_PORT'));
if ($port && $port != 80 && $port != 443)
{
$host .= ":$port";
}
}
return $host;
}
public function getHostUrl()
{
return $this->getProtocol() . '://' . $this->getHost();
}
/**
* @return string
*/
public function getRoutePath()
{
$xfRoute = $this->filter('_xfRoute', 'str');
if ($xfRoute)
{
return $xfRoute;
}
$routePath = ltrim($this->getExtendedUrl(), '/');
return $this->getRoutePathInternal($routePath);
}
public function getRoutePathFromExtended($extended)
{
$routePath = ltrim($extended, '/');
return $this->getRoutePathInternal($routePath);
}
public function getRoutePathFromUrl($url, bool $stripScript = false)
{
$url = $this->convertToAbsoluteUri($url);
$url = str_replace($this->getHostUrl(), '', $url);
if ($stripScript)
{
$url = preg_replace('#^/.*[a-z0-9-_]+\.php\?#i', '?', $url);
}
$routePath = ltrim($this->getExtendedUrl($url), '/');
return $this->getRoutePathInternal($routePath);
}
protected function getRoutePathInternal($routePath)
{
if (strlen($routePath) == 0)
{
return '';
}
if ($routePath[0] == '?')
{
$routePath = substr($routePath, 1);
$nextArg = strpos($routePath, '&');
if ($nextArg !== false)
{
$routePath = substr($routePath, 0, $nextArg);
}
if (strpos($routePath, '=') !== false)
{
return ''; // first bit has a "=" so it's named
}
}
else
{
$queryStart = strpos($routePath, '?');
if ($queryStart !== false)
{
$routePath = substr($routePath, 0, $queryStart);
}
}
return strval($routePath);
}
public function parseAcceptHeaderValue($headerValue)
{
$headerValue = trim($headerValue);
if (!$headerValue)
{
return [];
}
$accept = [];
foreach (explode(',', $headerValue) AS $acceptPart)
{
$acceptPart = trim($acceptPart);
if (!strlen($acceptPart))
{
continue;
}
$segments = explode(';', $acceptPart);
$type = trim($segments[0]);
if (!strlen($type))
{
continue;
}
unset($segments[0]);
$options = [];
foreach ($segments AS $segment)
{
$option = explode('=', $segment, 2);
if (isset($option[1]))
{
$options[trim($option[0])] = trim($option[1]);
}
else
{
$options[trim($option[0])] = true;
}
}
if (isset($options['q']))
{
$q = floatval($options['q']);
$q = max(0, min(1, $q));
unset($options['q']);
}
else
{
$q = 1;
}
$accept[] = [
'type' => $type,
'q' => $q,
'options' => $options
];
}
usort($accept, function($a, $b)
{
if ($a['q'] > $b['q'])
{
return -1;
}
else if ($a['q'] < $b['q'])
{
return 1;
}
$aTypeParts = explode('/', $a['type'], 2);
if (isset($aTypeParts[1]))
{
$aType = $aTypeParts[0];
$aSubType = $aTypeParts[1];
}
else
{
$aType = $a['type'];
$aSubType = null;
}
$bTypeParts = explode('/', $b['type'], 2);
if (isset($bTypeParts[1]))
{
$bType = $bTypeParts[0];
$bSubType = $bTypeParts[1];
}
else
{
$bType = $b['type'];
$bSubType = null;
}
if ($aType !== '*' && $bType === '*')
{
return -1;
}
else if ($aType === '*' && $bType !== '*')
{
return 1;
}
else if ($aType !== $bType)
{
// main types are different, so no comparison can be done
return 0;
}
// main types are now known to be the same
if ($aSubType !== null && $bSubType === null)
{
return -1;
}
else if ($aSubType === null && $bSubType !== null)
{
return 1;
}
if ($aSubType !== '*' && $bSubType === '*')
{
return -1;
}
else if ($aSubType === '*' && $bSubType !== '*')
{
return 1;
}
// sub-types may be different but have equal precedence
$aOptionCount = count($a['options']);
$bOptionCount = count($b['options']);
if ($aOptionCount > $bOptionCount)
{
return -1;
}
else if ($aOptionCount < $bOptionCount)
{
return 1;
}
// nothing else we can check, they're tied
return 0;
});
return $accept;
}
public function isEmbeddedImageRequest(): bool
{
$accept = $this->parseAcceptHeaderValue($this->getServer('HTTP_ACCEPT'));
if (!$accept)
{
return false;
}
$haveImageMatch = false;
foreach ($accept AS $acceptType)
{
$mimeType = strtolower($acceptType['type']);
switch (strtolower($mimeType))
{
case 'text/html':
case 'application/xhtml+xml':
// we're explicitly asking for HTML, so don't consider as an image request
return false;
}
if ($mimeType === '*/*')
{
// general match, don't count as this is almost always present
continue;
}
if (substr($mimeType, 0, 6) !== 'image/')
{
// we are accepting something that isn't an image, so don't consider this as an image request
return false;
}
$haveImageMatch = true;
}
return $haveImageMatch;
}
public function convertToAbsoluteUri($uri, $fullBasePath = null)
{
if (!$fullBasePath)
{
$fullBasePath = $this->getFullBasePath();
}
return \XF::convertToAbsoluteUrl($uri, $fullBasePath);
}
public function getInputFilterer()
{
return $this->filterer;
}
public function getNewArrayFilterer(array $input = [])
{
return $this->filterer->getNewArrayFilterer($input);
}
}