* About 80% of this class was re-written by Trevor Herselman
* But originally based on the zend-diactoros Uri class
* I took some ideas from .NET, Symfony, Zend, CodeIgniter etc.
*/
/**
* Zend Framework (http://framework.zend.com/)
*
* @see http://github.com/zendframework/zend-diactoros for the canonical source repository
* @copyright Copyright (c) 2015-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-diactoros/blob/master/LICENSE.md New BSD License
*/
use Psr\Http\Message\UriInterface;
/**
* Implementation of Psr\Http\Message\UriInterface.
*
* Provides a value object representing a URI for HTTP requests.
*
* Instances of this class are considered immutable; all methods that
* might change state are implemented such that they retain the internal
* state of the current instance and return a new instance that contains the
* changed state.
*/
class Uri implements UriInterface
{
/**
* Sub-delimiters used in query strings and fragments.
*
* @const string
*/
const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;=';
/**
* Unreserved characters used in paths, query strings, and fragments.
*
* @const string
*/
const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~\pL';
/**
* Idea taken from: https://msdn.microsoft.com/en-us/library/system.uri.getleftpart.aspx
* Used with getLeftPart()
*
* @const int
*/
const PARTIAL_SCHEME = 0;
const PARTIAL_AUTHORITY = 1;
const PARTIAL_PATH = 2;
const PARTIAL_QUERY = 3;
/**
* Idea taken from: https://msdn.microsoft.com/en-us/library/system.urikind.aspx
*
* @const int
*/
const KIND_ABSOLUTE = 0;
const KIND_PARTIAL = 1;
const KIND_ANY = 2;
// Unfinished idea, use an array index to store the components instead of private variables, since we don't usually use `user`, `pass`, `port` and `fragment`
/*
const URI = 0;
const SCHEME = 1;
const USER = 2;
const PASS = 3;
const HOST = 4;
const PORT = 5;
const PATH = 6;
const QUERY = 7;
const FRAG = 8;
*/
/**
* Idea taken from: https://msdn.microsoft.com/en-us/library/system.urihostnametype.aspx
*
* @const int
*/
const HOST_NAME_TYPE_BASIC = 0; // The host is set, but the type cannot be determined.
const HOST_NAME_TYPE_DNS = 1; // The host name is a domain name system (DNS) style host name.
const HOST_NAME_TYPE_IPV4 = 2; // The host name is an Internet Protocol (IP) version 4 host address.
const HOST_NAME_TYPE_IPV6 = 3; // The host name is an Internet Protocol (IP) version 6 host address.
const HOST_NAME_TYPE_UNKNOWN = 4; // The type of the host name is not supplied.
/**
* @var int[] Array indexed by valid scheme names to their corresponding ports.
* Idea to support `Trusted Proxes` and the "X-Forwarded-Proto" header, so isSecure() will return true even for 'http' requests that are routed through them
* `PHP uses a non-standards compliant practice of including brackets in query string fieldnames` eg. foo[]=1&foo[]=2&foo[]=3 ==> ['foo' => ['1', '2', '3']]
* the CGI standard way of handling duplicate query fields is to create an array, but PHP's parse_str() silently overwrites them; eg. foo=1&foo=2&foo=3 ==> ['foo' => '3']
* This parameter will
* @var bool
*/
static $standardArrays = false;
/**
* URI string cache ~ reset to null when any property changes
*
* @var null|string
*/
protected $_uri = null;
/**
* @var null|string
*/
protected $_scheme = null;
/**
* @var null|string
*/
// protected $_authority = null;
/**
* @var null|string
*/
// protected $_userInfo = null;
/**
* @var null|string
*/
protected $_user = null;
/**
* @var null|string
*/
protected $_pass = null;
/**
* @var null|string
*/
protected $_host = null;
/**
* @var null|int
*/
protected $_port = null;
/**
* @var null|string
*/
protected $_path = null;
/**
* @var null|string
*/
protected $_query = null;
/**
* @var null|string
*/
protected $_frag = null;
/**
* @param string $uri
* @throws InvalidArgumentException on non-string $uri argument
*/
public function __construct()
{
switch (func_num_args())
{
case 0:
// $_parts['original'] = null;
return;
case 1:
$args = func_get_args();
if (is_string($args[0]))
{
if ($args[0] === 'https' || $args[0] === 'http')
{
self::fromGlobals($this);
$this->_scheme = $args[0];
return;
}
else
{
$this->parse($args[0]);
return;
}
}
else if (is_array($args[0]))
$parts = &$args[0];
else if ($args[0] instanceof self)
{
// Copy constructor
$uri = &$args[0];
$this->_uri = $uri->_uri;
$this->_scheme = $uri->_scheme;
$this->_user = $uri->_user;
$this->_pass = $uri->_pass;
$this->_host = $uri->_host;
$this->_port = $uri->_port;
$this->_path = $uri->_path;
$this->_query = $uri->_query;
$this->_frag = $uri->_frag;
return;
}
else
throw new InvalidArgumentException(sprintf(
'Invalid argument type passed to Uri constructor; expecting a string or Uri object, received "%s"',
if ($args[0] instanceof self && $args[1] instanceof self)
{ // TODO: Build a Uri from base and relative Uri (`Initializes a new instance of the Uri class based on the combination of a specified base Uri instance and a relative Uri instance.`)
else // I want to support a constructor that takes a baseUri/absoluteUri and relativeUri eg. 'http://example.com/admin/' + '../login' => 'http://example.com/login'
{
$parts = &$args; // instead of creating a new array, use the existing one! Basically 'scheme' and 'host' will be added to $args
$parts['scheme'] = &$args[0];
$parts['host'] = &$args[1];
}
break;
case 3:
case 4:
case 5:
// Build a Uri string with 'scheme' + 'host' + ('path' + 'query' + 'fragment')
// Does not support `user`, `pass` and `port`
$args = func_get_args();
$parts = &$args;
$parts['scheme'] = &$args[0];
$parts['host'] = &$args[1];
if (isset($args[2]))
{
$parts['path'] = &$args[2];
if (isset($args[3]))
{
$parts['query'] = &$args[3];
if (isset($args[4]))
$parts['fragment'] = &$args[4];
}
}
break;
default;
throw new InvalidArgumentException(
'Too many arguments passed to Uri constructor!'
);
}
$this->fromArray($parts);
}
public static function fromGlobals(&$ptr = null)
{
static $uri;
if (empty($uri))
{
if (empty($_SERVER['HTTP_HOST']))
throw new \Exception('$_SERVER[HTTP_HOST] is empty');
if (empty($_SERVER['REQUEST_URI']))
throw new \Exception('$_SERVER[REQUEST_URI] is empty');
if ( ! isset($_SERVER['QUERY_STRING'])) // QUERY_STRING can be empty!
throw new \Exception('$_SERVER[QUERY_STRING] is not set');
// $uri->_path = preg_replace('#^[^/:]+://[^/]+#', '', $_SERVER['REQUEST_URI']); // from marshalRequestUri(), but this includes the query string // alternative 4
if ( ! empty($_SERVER['QUERY_STRING']))
$uri->_query = $_SERVER['QUERY_STRING'];
}
if ($ptr === null)
return clone $uri;
if ($ptr instanceof self)
{
$ptr->_uri = (string) $uri;
$ptr->_scheme = $uri->_scheme;
$ptr->_user = $uri->_user;
$ptr->_pass = $uri->_pass;
$ptr->_host = $uri->_host;
$ptr->_port = $uri->_port;
$ptr->_path = $uri->_path;
$ptr->_query = $uri->_query;
$ptr->_frag = $uri->_frag;
}
else if (is_array($ptr))
{
$ptr['scheme'] = $uri->_scheme;
$ptr['user'] = $uri->_user;
$ptr['pass'] = $uri->_pass;
$ptr['host'] = $uri->_host;
if (isset($uri->_port))
$ptr['port'] = $uri->_port;
$ptr['path'] = $uri->_path;
if ($uri->_query)
$ptr['query'] = $uri->_query;
if ($uri->_frag)
$ptr['fragment'] = $uri->_frag;
}
return $ptr;
}
// Alias of fromGlobals() to support Symfony
public static function createFromGlobals(&$ptr = null)
{
return self::fromGlobals($ptr);
}
public function reset()
{
$this->_uri = null;
$this->_scheme = null;
$this->_user = null;
$this->_pass = null;
$this->_host = null;
$this->_port = null;
$this->_path = null;
$this->_query = null;
$this->_frag = null;
}
public function isDomain($domain) // eg. isDomain('.com') || isDomain('example.com') ... NOTE: For performance reasons, we don't strtolower() the $domain!
throw new \InvalidArgumentException("The source email address `{$uri}` appears to be malformed");
$parts = parse_url('mailto://' . $parts['path']); // we 'fool' the parser by adding '//' before the email address, this allows parse_url() to detect the 'authority` components (user@domain)
if ($parts === false)
throw new \InvalidArgumentException("The source email address `{$uri}` appears to be malformed");
}
$this->fromArray($parts);
}
/**
* Parse a URI array into its parts, and set the properties, $parts must be compatible with parse_url()
*
* @param array $parts
*/
public function fromArray(array $parts)
{ // runs parameters through __set()
$this->scheme = $parts['scheme'] ?? null;
$this->user = $parts['user'] ?? null;
$this->pass = $parts['pass'] ?? null;
$this->host = $parts['host'] ?? null;
$this->port = $parts['port'] ?? null;
$this->path = $parts['path'] ?? null;
$this->query = $parts['query'] ?? null;
$this->fragment = $parts['fragment'] ?? null;
}
/**
* builds/returns an array compatible with the result from parse_url()
*
* @param string $uri
* @return array compatible with parse_url()
*/
public function &toArray()
{
$arr = [];
if ($this->_scheme !== null)
$arr['scheme'] = $this->_scheme;
if ($this->_user !== null)
$arr['user'] = $this->_user;
if ($this->_pass !== null)
$arr['pass'] = $this->_pass;
if ($this->_host !== null)
$arr['host'] = $this->_host;
if ($this->_port !== null)
$arr['port'] = $this->_port;
if ($this->_path !== null)
$arr['path'] = $this->_path;
if ($this->_query !== null)
$arr['query'] = $this->_query;
if ($this->_frag !== null)
$arr['fragment'] = $this->_frag;
return $arr;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* Similar to Symfony isSecure() http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html#method_isSecure
* Symfony has additional checks, so if an 'http' request is routed via X-Forwarded-Proto through a `Trusted Proxy` then it also returns true!
* Taken from: https://msdn.microsoft.com/en-us/library/system.uri.segments.aspx
*
* Taken from MSDN:
* `The Segments property returns an array of strings containing the "segments" (substrings) that form the URI's absolute path.
* The first segment is obtained by parsing the absolute path from its first character until you reach a slash (/) or the end of the path.
* Each additional segment begins at the first character after the preceding segment, and terminates with the next slash or the end of the path.
* (A URI's absolute path contains everything after the host and port and before the query and fragment.)`
*
* `Note that because the absolute path starts with a '/', the first segment contains it and nothing else.`
*/
public function getSegments()
{
return $this->segments;
/*
// Alternative version using explode() and array internal pointers
if ($this->path !== null)
{
$segments = explode('/', $this->path);
if (empty(end($segments)))
array_pop($segments); // this normally indicates that the last character was a '/', which created an empty string in the last array member
else
prev($segments); // reverse the internal pointer one place (from the end), because we have a 'file' name and we don't want to append '/' to something like 'file.php'
for ( ;($key = key($segments)) !== null; prev($segments))
$segments[$key] .= '/';
}
else
return [];
*/
// old version that excludes the '/' ... we could modify this with a foreach loop that adds the '/' to the end of each string!
//return explode('/', $this->path ?: '');
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return string[] Returns an array of the `host` name split into DNS segments; eg. example.com => ['example', '.com']
*/
public function getDnsSegments()
{
return $this->dnsSegments;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* Similar output to the getSegments() function but excludes all the trailing '/'
*/
public function getSegmentsExplode()
{
if ($this->_path === null)
return [];
$segments = explode('/', $this->_path);
if (empty(end($segments)))
array_pop($segments); // this normally indicates that the last character was a '/', which created an empty string in the last array member
public function setUserInfo($user = null, $password = null)
{
$this->user = $user;
$this->pass = $password;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setHost($host)
{
$this->host = $host;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setPort($port)
{
$this->port = $port;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setPath($path)
{
$this->path = $path;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setQuery($query)
{
$this->query = $query;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setFragment($fragment)
{
$this->fragment = $fragment;
return $this;
}
/**
* Is a given port non-standard for the current scheme?
* WARNING: This retarded function from Zend requires $port to be an integer, or "$port !== self::$allowedSchemes[$scheme]" will fail! I was testing it against $_SERVER['SERVER_PORT'] which is a string!
*
* @param string $scheme
* @param string $host
* @param int $port
* @return bool
*/
static function isNonStandardPort($scheme, $host, $port)
* Name taken from .NET Framework: https://msdn.microsoft.com/en-us/library/system.uri.gethashcode.aspx
* By default it uses `crc32`, which I think returns the hex values, but I think .NET returns the integer value, problem is with the negative values !?!?
*
* @return int
*/
public function getHashCode(string $algo = 'crc32', bool $raw_output = false)
{
return hash($algo, (string) $this, $raw_output);
}
/**
* MODIFIED by Trevor Herselman on 1 July 2017 @ 7pm. Changed the preg_replace to substr() and moved the `if empty()` test before the strtolower()
*
* Encodes the scheme to ensure it is a valid scheme.
case 'hostAndRealPort': // INCLUDES the default port
$result = $this->host;
if ()
return $result
*/
default: // couldn't find the property with common default names; so lets test the property for a mixed-case name or hash algorithm
if ( ! ctype_lower($name))
$name = strtolower($name);
if (self::$hashAlgos === null)
self::$hashAlgos = array_flip(hash_algos()); // set the hash algorithms as keys for faster lookup with isset() instead of in_array()!
if (isset(self::$hashAlgos[$name])) // we converted the hash name to lowercase above so we can safely support this: $this->Sha256
return hash($name, (string) $this);
//--- Start of alias and mixed-case properties ---//
switch ($name)
{
// possible mixed-case variants `normalized` to lowercase. eg. `Scheme` => `scheme`
case 'scheme': return $this->_scheme;
case 'host': return $this->_host;
case 'path': return $this->_path;
case 'query': return $this->_query;
case 'user': return $this->_user;
case 'pass': return $this->_pass;
case 'port': return $this->_port;
case 'fragment': return $this->_frag;
case 'realport': // My alias of .NET `Uri.StrongPort`
case 'strongport':
// This is called the Uri.StrongPort property in .NET Framework
// `The Port data. If no port data is in the Uri and a default port has been assigned to the Scheme,
// the default port is returned. If there is no default port, -1 is returned.`
// I put this property here because you are free to use ANY mixed/camel/pascal case variant of this property: eg. StrongPort, strongPort, realPort, RealPort etc.
case 'userinfo': return $this->userInfo; // mixed-case variant
case 'userpass': return $this->userInfo; // Alias of userInfo
case 'segments': return $this->__get($name); // $this->segments;
case 'authority': return $this->__get($name); // $this->authority;
case 'schemeandserver': return $this->schemeAndServer;
case 'hostandport': return $this->hostAndPort;
case 'querystring': return $this->_query; // possible mixed-case variant of `queryString`
case 'username': return $this->_user;
case 'password': return $this->_pass;
case 'uri': return (string) $this;
case 'tostring': return (string) $this;
case 'string': return (string) $this;
}
}
$trace = debug_backtrace();
trigger_error('Undefined property: ' . __CLASS__ . "->{$name} in <b>{$trace[0]['file']}</b> on line <b>{$trace[0]['line']}</b>; thrown", E_USER_ERROR);
}
// requires 13 case matches before an array lookup table becomes viable, unlikely to ever become viable
function __set($name, $value)
{
$this->_uri = null;
$property = &$name;
if ( ! ctype_lower($property))
$property = strtolower($property);
switch ($property)
{
case 'scheme':
if (is_string($value))
{
$scheme = &$value;
if ( ! ctype_lower($scheme))
{
// Try to fix the (invalid) scheme by removing trailing ':' or '://' and converting to lowercase
if ( ! array_key_exists($scheme, self::$allowedSchemes) && ! empty($scheme))
{
7 years ago
throw new InvalidArgumentException("Unsupported Uri scheme `{$scheme}`; must be an empty string or in the set (" . implode(', ', array_keys(self::$allowedSchemes)) . ')');
case 'userinfo': return $this->userInfo; // mixed-case variant
case 'segments': return $this->segments;
case 'authority': return $this->authority;
case 'querystring': return $this->query; // possible mixed-case variant of `queryString`
case 'username': return $this->user;
case 'password': return $this->pass;
case 'uri': return (string) $this;
case 'tostring': return (string) $this;
}
}
$trace = debug_backtrace();
trigger_error('Undefined property: ' . __CLASS__ . "->{$name} in <b>{$trace[0]['file']}</b> on line <b>{$trace[0]['line']}</b>; thrown", E_USER_ERROR);