A fast and light-weight component library
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2287 lines
63 KiB

<?php
namespace Twister;
/**
* 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.
*/
static $allowedSchemes = [
'http' => 80,
'https' => 443,
'ftp' => 21, // ftp://[user[:password]@]host[:port]/url-path
'mailto' => true, // mailto:name@email.com
'file' => true, // file://host/path file://localhost/etc/fstab == file:///etc/fstab file://localhost/c:/WINDOWS/clock.avi == file:///c:/WINDOWS/clock.avi
'php' => true // php://stdin, php://stdout and php://stderr, php://input etc. http://php.net/manual/en/wrappers.php.php
];
// currently unused!
static $authorityPrefix = [
'http' => '//',
'https' => '//',
'ftp' => '//', // ftp://[user[:password]@]host[:port]/url-path
'mailto' => null, // mailto:name@email.com
'file' => '//', // file://host/path file://localhost/etc/fstab == file:///etc/fstab file://localhost/c:/WINDOWS/clock.avi == file:///c:/WINDOWS/clock.avi
'php' => '//' // php://stdin, php://stdout and php://stderr, php://input etc. http://php.net/manual/en/wrappers.php.php
];
/**
* Array of supported hash algoithms, initialized to hash_algos() on first use!
* Used when generating dynamic hash properties eg. $this->md5
* Some algorithms cannot be used such as `gost-crypto`, `tiger128,3` etc. because of invalid characters in the name.
* We could create a mapping from `gost-crypto` to `gost_crypto` and `tiger128,3` to `tiger128_3` etc. replacing invalid characters with underscores
* But that would require looping through the array, and most of those algorithms are not very useful for Uri hashing.
* http://php.net/manual/en/function.hash-algos.php
* @var string[] Array of supported hash algoithms as array keys for fast lookup with isset(), used to test for dynamic hash properties eg. $this->md5
*/
static $hashAlgos = null;
/**
* Currently UNUSED: cannot use `gost-crypto`, `tiger128,3` etc.
* Maybe we can create a mapping from `gost-crypto` to `gost_crypto` and `tiger128,3` to `tiger128_3` etc. replacing invalid characters with underscore
*/
/*
static $validHashAlgos = [ 'md2', 'md4', 'md5',
'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
'ripemd128', 'ripemd160', 'ripemd256', 'ripemd320',
'whirlpool', 'snefru', 'snefru256', 'gost',
'adler32', 'crc32', 'crc32b', 'fnv132', 'fnv1a32', 'fnv164', 'fnv1a64', 'joaat'
];
*/
/**
* Idea taken from CakePHP: https://api.cakephp.org/2.3/class-CakeRequest.html#$_detectors
* https://api.cakephp.org/2.3/source-class-CakeRequest.html#92-117
*
* @var mixed[] Array of built in detectors used with is($type) or is$type(), can be modified with addDetector().
*/
// TODO
static $_detectors = null; // ['dotcom' => function(&$uri){return substr($uri->host, -4) === '.com'}] eg. isDotCom() || is('DotCom') || is('.com') (the '.com' version cannot be tested with is.com()!)
// ['domain' => function(&$uri, $domain){return substr($uri->host, -strlen($uri->host)) === $domain}] // isDomain('example.com') || isDomain('.com') || is('domain', '.com')
/**
* TODO: Not implemented yet!
* 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
* See: http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html#method_setTrustedProxies
* @var bool[]
*/
static $trustedProxies = [];
/**
* `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"',
(is_object($args[0]) ? get_class($args[0]) : gettype($args[0]))
));
break;
case 2:
$args = func_get_args();
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.`)
// https://msdn.microsoft.com/en-us/library/ceyeze4f(v=vs.110).aspx
}
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 = new Uri();
$uri->_scheme = ($_SERVER['HTTPS'] ?? null) === 'on' ? 'https' : 'http';
$uri->_host = empty($_SERVER['HTTP_HOST']) ? (empty($_SERVER['SERVER_NAME']) ? getenv('SERVER_NAME') : $_SERVER['SERVER_NAME']) : strtolower($_SERVER['HTTP_HOST']);
if (self::isNonStandardPort($uri->_scheme, $uri->_host, (int) ($_SERVER['SERVER_PORT'] ?? 0)))
$uri->_port = (int) $_SERVER['SERVER_PORT'];
$uri->_path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); // alternative 1
// $uri->_path = parse_url(rawurldecode($_SERVER['REQUEST_URI']), PHP_URL_PATH) // alternative 2
// $uri->_path = ($qpos = strpos($_SERVER['REQUEST_URI'], '?')) !== false ? substr($_SERVER['REQUEST_URI'], 0, $qpos) : $_SERVER['REQUEST_URI']; // alternative 3
// $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!
{
if ($this->_host === null) return false;
return substr($this->_host, -strlen($this->_host)) === $domain;
}
public function isHost($host) // alias of isDomain()
{
return $this->isDomain($host);
}
/**
* Strip the query string from a path
* Taken from zend-diactoros ServerRequestFactory.php on line 368
*
* @param mixed $path
* @return string
*/
static function stripQueryString($path)
{
return ($qpos = strpos($path, '?')) !== false ? substr($path, 0, $qpos) : $path;
}
/**
* TODO: Not implemented yet, just returns $qs
*
* Idea taken from http://api.symfony.com/2.3/Symfony/Component/HttpFoundation/Request.html#method_normalizeQueryString
* `It builds a normalized query string, where keys/value pairs are alphabetized, have consistent escaping and unneeded delimiters are removed.`
*
* @param mixed $path
* @return string
*/
static function normalizeQueryString(string $qs)
{
return $qs;
}
/**
* Returns an array of the host split by '.'
*
* @return string[] Array of host value split by '.', can be an IPv4 address or Domain with TLD's
*/
public function splitHost() // AKA explodeHost()
{
return explode('.', $this->_host ?? '');
}
/**
* Returns an array of the path split by '/'
*
* @return string[] Array of path value split by '/', remember that absolute paths start with /, so the first array value is usually an empty string!
*/
public function splitPath() // AKA explodePath()
{
return explode('/', $this->_path ?? '');
}
/**
* Parse a URI into its component parts, and set the properties
*
* @param string $uri
*/
private function parse($uri)
{
$parts = parse_url($uri);
if ($parts === false)
throw new \InvalidArgumentException("The source URI `{$uri}` appears to be malformed");
// special handling of mailto: scheme
if (($parts['scheme'] ?? null) === 'mailto')
{
if (isset($parts['host']) || isset($parts['user']) || isset($parts['pass']) || isset($parts['fragment']) || isset($parts['query']) || isset($parts['port']) || empty($parts['path']) || $parts['path'][0] === '/')
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!
*
* @return bool $this->scheme === 'https';
*/
public function isSecure()
{
return $this->_scheme === 'https';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'https';
*/
public function isHttps()
{
return $this->_scheme === 'https';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'http';
*/
public function isHttp()
{
return $this->_scheme === 'http';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'http' || $this->scheme === 'https';
*/
public function isHttpOrHttps()
{
return $this->_scheme === 'http' || $this->_scheme === 'https';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'ftp';
*/
public function isFtp()
{
return $this->_scheme === 'ftp';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'https';
*/
public function isMailto()
{
return $this->_scheme === 'mailto';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return bool $this->scheme === 'file';
*/
public function isFile()
{
return $this->_scheme === 'file';
}
/**
* Return the string representation as a URI reference.
*
* Depending on which components of the URI are present, the resulting
* string is either a full URI or relative reference according to RFC 3986,
* Section 4.1. The method concatenates the various components of the URI,
* using the appropriate delimiters:
*
* - If a scheme is present, it MUST be suffixed by ":".
* - If an authority is present, it MUST be prefixed by "//".
* - The path can be concatenated without delimiters. But there are two
* cases where the path has to be adjusted to make the URI reference
* valid as PHP does not allow to throw an exception in __toString():
* - If the path is rootless and an authority is present, the path MUST
* be prefixed by "/".
* - If the path is starting with more than one "/" and no authority is
* present, the starting slashes MUST be reduced to one.
* - If a query is present, it MUST be prefixed by "?".
* - If a fragment is present, it MUST be prefixed by "#".
*
* @see http://tools.ietf.org/html/rfc3986#section-4.1
* @return string
*/
public function __toString()
{
if ($this->_uri === null)
{
$uri = &$this->_uri;
if ($this->_scheme !== null)
$uri .= $this->_scheme . ':';
if ($this->_host !== null)
$uri .= ($this->_scheme !== 'mailto' ? '//' : null) . $this->authority; // warning: `mailto:user@example.com` doesn't have the '//' prefix!
if ($this->_path !== null)
{
if ($this->_path[0] !== '/' && $this->_host !== null)
$uri .= '/';
$uri .= $this->_path;
//$uri .= static::encodePath($this->path); // Zend version
}
else if ($this->_host !== null && ($this->_query || $this->_frag))
$uri .= '/';
if ($this->_query !== null)
$uri .= '?' . $this->_query; // $uri .= '?' . static::encodeQueryFragment($this->query); ZEND
if ($this->_frag !== null)
$uri .= '#' . $this->_frag; // $uri .= "#" . static::encodeQueryFragment($this->fragment); ZEND
}
return $this->_uri;
}
/**
* {@inheritdoc}
*/
public function getScheme()
{
return $this->_scheme ?: '';
}
/**
* {@inheritdoc}
*/
public function getAuthority()
{
return $this->authority ?: '';
}
/**
* {@inheritdoc}
*/
public function getUserInfo()
{
return $this->userInfo ?: '';
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
*/
public function getUser()
{
return $this->_user ?: '';
// return empty($this->userInfo) ? '' : (($pos = strpos($this->userInfo, ':')) === false ? $this->userInfo : substr($this->userInfo, 0, $pos));
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
*/
public function getPassword()
{
return $this->_pass ?: '';
// return empty($this->userInfo) ? '' : (($pos = strpos($this->userInfo, ':')) === false ? '' : substr($this->userInfo, $pos + 1));
}
/**
* {@inheritdoc}
*/
public function getHost()
{
return $this->_host ?: '';
}
/**
* {@inheritdoc}
*/
public function getPort()
{
return self::isNonStandardPort($this->_scheme, $this->_host, $this->_port)
? $this->_port
: null;
}
/**
* NON-PSR-7 function added by Trevor Herselman
* .NET Framework always returns the default port number, even if it's not given in the URI.
* So I just want to have a function that returns the default port number when one isn't specified!
*
* `The port number defines the protocol port used for contacting the server referenced in the URI.
* If a port is not specified as part of the URI, the Port property returns the default value for the protocol.
* If there is no default port number, this property returns -1.`
*
* @return integer
*/
public function getRealPort()
{
return $this->_port ?: (self::$allowedSchemes[$this->_scheme] ?? -1);
}
/**
* {@inheritdoc}
*/
public function getPath()
{
return $this->_path ?: '';
}
/**
* {@inheritdoc}
*/
public function getQuery()
{
return $this->_query ?: '';
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Alias for getQuery() to support Symfony syntax
*/
public function getQueryString()
{
return $this->_query ?: '';
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Based on the Zend framework Uri class method with the same name!
* https://framework.zend.com/manual/2.4/en/modules/zend.uri.html#getting-the-query-part-of-the-uri
*/
public function getQueryAsArray()
{
parse_str($this->_query, $result);
return $result;
}
/**
* {@inheritdoc}
*/
public function getFragment()
{
return $this->_frag ?: '';
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Idea taken from the Uri property `PathAndQuery` in .NET Framework: https://msdn.microsoft.com/en-us/library/system.uri.pathandquery.aspx
* AKA REQUEST_URI
*/
public function getPathAndQuery()
{
return $this->_path . ($this->_query === null ? null : '?' . $this->_query);
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Taken from: https://msdn.microsoft.com/en-us/library/system.uri.getleftpart.aspx
*
* `The GetLeftPart method returns a string containing the leftmost portion of the URI string, ending with the portion specified by part.`
*
* Taken from MSDN: `Gets the specified portion of a Uri instance.`
*/
public function getLeftPart($part)
{
$new = new static();
// $uri = null;
if (is_string($part))
{
switch ($part)
{
case 'scheme': $part = self::PARTIAL_SCHEME; break;
case 'authority': $part = self::PARTIAL_AUTHORITY; break;
case 'path': $part = self::PARTIAL_PATH; break;
case 'query': $part = self::PARTIAL_QUERY; break;
default:
throw new \InvalidArgumentException("Invalid part `{$part}` for Uri->getLeftPart(); Only `scheme`, `authority`, `path` and `query` are valid!");
}
}
switch ($part)
{
case self::PARTIAL_QUERY:
$new->_query = $this->_query;
// if ($this->_query !== null)
// $uri = '?' . $this->_query;
case self::PARTIAL_PATH:
$new->_path = $this->_path;
// if ($this->_path !== null)
// $uri = ($this->_path[0] === '/' ? null : '/') . $this->_path . $uri;
// else if ($this->_host !== null)
// $uri = '/' . $uri;
case self::PARTIAL_AUTHORITY:
$new->_user = $this->_user;
$new->_pass = $this->_pass;
$new->_host = $this->_host;
$new->_port = $this->_port;
// $new->authority = $this->authority;
// if ($this->_host !== null)
// $uri = ($this->_scheme === 'mailto' ? null : '//') . $this->authority . $uri;
case self::PARTIAL_SCHEME:
$new->_scheme = $this->_scheme;
// if ($this->_scheme !== null)
// $uri = $this->_scheme . ':' . $uri;
}
return $new;
// return $uri;
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Taken from: https://msdn.microsoft.com/en-us/library/ms131572.aspx
*
* This version accepts a `result` pointer (if func_num_args() === 2)
* which will be set/reset with the current object and returns true/false on success/failure,
* or if result is NOT provided (if func_num_args() === 1) then will return the new object directly!
*
* Taken from MSDN:
* `Creates a new Uri using the specified String instance and a UriKind.`
* `If this method returns true, the new Uri is in result.`
*
* @return bool
*/
public function tryCreate($uri, &$result = null) // TODO
{
$parts = parse_url($uri);
if ($parts === false || empty($parts))
return func_num_args() === 1 ? $result : false;
switch (func_num_args())
{
case 1:
case 2:
if ($result instanceof self)
{
}
else
{
}
default:
}
return func_num_args() === 1 ? $result : true;
}
/**
* NON-PSR-7 function added by Trevor Herselman
* Similar to .NET Finalize; frees memory associated with the Uri
*
*/
public function finalize()
{
if ($this->_uri === null)
$this->_uri = (string) $this;
/*
$this->_scheme = null;
$this->_user = null;
$this->_pass = null;
$this->_host = null;
$this->_port = null;
$this->_path = null;
$this->_query = null;
$this->_freq = null;
*/
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
* My idea, just a wrapper around preg_match()
*/
public function match($pattern, &$matches = null, $flags = 0, $offset = 0)
{
return preg_match($pattern, (string) $this, $matches, $flags, $offset);
}
/**
* NON-PSR-7 function added by Trevor Herselman
*/
public function matchHost($pattern, &$matches = null, $flags = 0, $offset = 0)
{
return preg_match($pattern, $this->_host ?: '', $matches, $flags, $offset);
}
/**
* NON-PSR-7 function added by Trevor Herselman
*/
public function matchPath($pattern, &$matches = null, $flags = 0, $offset = 0)
{
return preg_match($pattern, $this->_path ?: '', $matches, $flags, $offset);
}
/**
* NON-PSR-7 function added by Trevor Herselman
*/
public function matchQuery($pattern, &$matches = null, $flags = 0, $offset = 0)
{
return preg_match($pattern, $this->_query ?: '', $matches, $flags, $offset);
}
/**
* NON-PSR-7 function added by Trevor Herselman
* 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
return $segments;
}
/**
* {@inheritdoc}
*/
public function withScheme($scheme)
{
if ( ! is_string($scheme) && $scheme !== null)
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
(is_object($scheme) ? get_class($scheme) : gettype($scheme))
));
$new = clone $this;
$new->scheme = $scheme;
return $new;
}
/**
* {@inheritdoc}
*/
public function withUserInfo($user, $password = null)
{
if ( ! is_string($user))
throw new InvalidArgumentException(sprintf(
'%s expects a string user argument; received %s',
__METHOD__,
(is_object($user) ? get_class($user) : gettype($user))
));
if ($password !== null && ! is_string($password))
throw new InvalidArgumentException(sprintf(
'%s expects a string password argument; received %s',
__METHOD__,
(is_object($password) ? get_class($password) : gettype($password))
));
$new = clone $this;
$new->userInfo = $user . ($password ? ':' . $password : null);
return $new;
}
/**
* {@inheritdoc}
*/
public function withHost($host)
{
if ( ! is_string($host))
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
(is_object($host) ? get_class($host) : gettype($host))
));
$new = clone $this;
$new->host = $host;
return $new;
}
/**
* {@inheritdoc}
*/
public function withPort($port)
{
if ( ! is_numeric($port) && $port !== null)
throw new InvalidArgumentException(sprintf(
'Invalid port "%s" specified; must be an integer, an integer string, or null',
(is_object($port) ? get_class($port) : gettype($port))
));
if ($port !== null)
$port = (int) $port;
if ($port === $this->port)
return clone $this; // Do nothing if no change was made.
if ($port !== null && $port < 1 || $port > 65535)
throw new InvalidArgumentException(sprintf(
'Invalid port "%d" specified; must be a valid TCP/UDP port',
$port
));
$new = clone $this;
$new->port = $port;
return $new;
}
/**
* {@inheritdoc}
*/
public function withPath($path)
{
if ( ! is_string($path))
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
(is_object($path) ? get_class($path) : gettype($path))
));
if (strpos($path, '?') !== false)
throw new InvalidArgumentException(
'Invalid path provided; paths must not contain a query string'
);
if (strpos($path, '#') !== false)
throw new InvalidArgumentException(
'Invalid path provided; paths must not contain a URI fragment'
);
$new = clone $this;
$new->path = $path;
return $new;
}
/**
* {@inheritdoc}
*/
public function withQuery($query)
{
if ( ! is_string($query))
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
(is_object($query) ? get_class($query) : gettype($query))
));
if (strpos($query, '#') !== false)
throw new InvalidArgumentException(
'Query string must not include a URI fragment'
);
$new = clone $this;
$new->query = $query;
return $new;
}
/**
* {@inheritdoc}
*/
public function withFragment($fragment)
{
if ( ! is_string($fragment))
throw new InvalidArgumentException(sprintf(
'%s expects a string argument; received %s',
__METHOD__,
(is_object($fragment) ? get_class($fragment) : gettype($fragment))
));
$new = clone $this;
$new->fragment = $fragment;
return $new;
}
/**
* Create a new Uri from parts array into its parts, and set the properties, $parts must be compatible with parse_url()
*
* @param array $parts
*/
public function withArray(array $parts = null)
{ // WARNING: Cannot use ?? because we might want to set some parts explicity to null, eg. ['fragment' => null]
// UPDATE: we could do this: ['fragment' => '']
$new = new static();
$new->scheme = isset($parts['scheme']) ? $parts['scheme'] : $this->_scheme;
$new->user = isset($parts['user']) ? $parts['user'] : $this->_user;
$new->pass = isset($parts['pass']) ? $parts['pass'] : $this->_pass;
$new->host = isset($parts['host']) ? $parts['host'] : $this->_host;
$new->port = isset($parts['port']) ? $parts['port'] : $this->_port;
$new->path = isset($parts['path']) ? $parts['path'] : $this->_path;
$new->query = isset($parts['query']) ? $parts['query'] : $this->_query;
$new->fragment = isset($parts['fragment']) ? $parts['fragment'] : $this->_fragment;
return $new;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
public function setScheme($scheme)
{
$this->scheme = $scheme;
return $this;
}
/**
* NON-PSR-7 function added by Trevor Herselman
*
* @return $this;
*/
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)
{
if ( ! $scheme)
return ($host && ! $port) ? false : true;
if ( ! $host || ! $port)
return false;
return ! isset(self::$allowedSchemes[$scheme]) || $port !== self::$allowedSchemes[$scheme];
}
/**
* Name taken from .NET Framework: https://msdn.microsoft.com/en-us/library/system.uri.isdefaultport.aspx
*
* @return bool
*/
public function isDefaultPort(int $port = null)
{
$port = $port ?: $this->port;
return $port === null || $port === (self::$allowedSchemes[$this->_scheme] ?? null);
}
/**
* 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.
*
* @param string $scheme Scheme name.
*
* @return string Encoded scheme.
*/
private function encodeScheme($scheme)
{
if (strpos($scheme, ':') !== false)
$scheme = substr($scheme, 0, strpos($scheme, ':')); // remove trailing : and optional trailing '//' characters ... eg. ('http://' || 'http:') -> 'http'
if (empty($scheme))
return null;
$scheme = strtolower($scheme);
if ( ! array_key_exists($scheme, self::$allowedSchemes))
throw new InvalidArgumentException(sprintf(
'Unsupported scheme "%s"; must be any empty string or in the set (%s)',
$scheme,
implode(', ', array_keys(self::$allowedSchemes))
));
return $scheme;
}
/**
* Encodes the path of a URI to ensure it is properly encoded.
*
* @param string $path
* @return string
*/
/**
* Encode the path
*
* Will replace all characters which are not strictly allowed in the path
* part with percent-encoded representation
*
* @param string $path
* @return string
*/
public static function encodePath($path)
{
$path = preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . ':@&=\+\$,\/;%]+|%(?![A-Fa-f0-9]{2}))/u',
__CLASS__ . '::urlEncodeChar',
$path
);
if (empty($path))
return null; // No path
if ($path[0] !== '/')
return $path; // Relative path
// Ensure only one leading slash, to prevent XSS attempts.
return '/' . ltrim($path, '/');
}
/**
* Encode a query string to ensure it is propertly encoded.
*
* Ensures that the values in the query string are properly urlencoded.
*
* @param string $query
* @return string
*/
private function encodeQuery($query, $arg_separator = '&')
{
if ( ! empty($query) && strpos($query, '?') === 0)
$query = substr($query, 1);
$parts = explode($arg_separator, $query);
foreach ($parts as $index => $part)
{
list($key, $value) = $this->splitQueryValue($part);
if ($value === null)
{
$parts[$index] = $this->encodeQueryOrFragment($key);
continue;
}
$parts[$index] = sprintf(
'%s=%s',
$this->encodeQueryOrFragment($key),
$this->encodeQueryOrFragment($value)
);
}
return implode($arg_separator, $parts);
}
/**
* Encodes a fragment value to ensure it is properly encoded.
*
* @param null|string $fragment
* @return string
*/
private function encodeFragment($fragment)
{
if ( ! empty($fragment) && $fragment[0] === '#')
$fragment = '%23' . substr($fragment, 1);
return $this->encodeQueryOrFragment($fragment);
}
/**
* Encodes a query string key or value, or a fragment.
*
* @param string $value
* @return string
*/
private function encodeQueryOrFragment($value)
{
return preg_replace_callback(
'/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]+|%(?![A-Fa-f0-9]{2}))/u',
__CLASS__ . '::urlEncodeChar',
$value
);
}
/**
* URL encode a character returned by a regex.
*
* @param array $matches
* @return string
*/
private static function urlEncodeChar(array $matches)
{
return rawurlencode($matches[0]);
}
/**
* Idea taken from: https://msdn.microsoft.com/en-us/library/system.uri.checkhostname.aspx
* `The CheckHostName method checks that the host name provided meets the requirements for a valid Internet host name.
* It does not, however, perform a host-name lookup to verify the existence of the host.`
*
* @param string $value
* @return string
*/
private function checkHostName() // this is a static method in .NET framework
{
// todo
}
/**
* Split a query value into a key/value tuple.
*
* @param string $value
* @return array A value with exactly two elements, key and value
*/
private function splitQueryValue($value)
{
$data = explode('=', $value, 2);
if (count($data) === 1)
$data[] = null;
return $data;
}
/**
* Added by Trevor Herselman on 2 July 2017
* Originally taken from: http://php.net/manual/en/function.parse-str.php#76792
*
* @param string $str
* @return array key-value pairs
*/
static function proper_parse_str($str)
{
// result array
$arr = array();
// split on outer delimiter
$pairs = explode('&', $str);
// loop through each pair
foreach ($pairs as $i)
{
// split into name and value
list($name,$value) = explode('=', $i, 2);
// if name already exists
if( isset($arr[$name]) )
{
// stick multiple values into an array
if( is_array($arr[$name]) )
$arr[$name][] = $value;
else
$arr[$name] = array($arr[$name], $value);
}
// otherwise, simply stick it in a scalar
else
$arr[$name] = $value;
}
// return result array
return $arr;
}
/**
* Added by Trevor Herselman on 2 July 2017
* Originally taken from: http://php.net/manual/en/function.parse-str.php#76792
* UNFINISHED updated version, just added $arg_separator so far
*
* @param string $value
* @return array A value with exactly two elements, key and value
*/
static function proper_parse_str_new($str, $arg_separator = '&')
{
// result array
$arr = array();
// split on outer delimiter
$pairs = explode($arg_separator, $str);
// loop through each pair
foreach ($pairs as $i)
{
// split into name and value
list($name,$value) = explode('=', $i, 2);
// if name already exists
if( isset($arr[$name]) )
{
// stick multiple values into an array
if( is_array($arr[$name]) )
$arr[$name][] = $value;
else
$arr[$name] = array($arr[$name], $value);
}
// otherwise, simply stick it in a scalar
else
$arr[$name] = $value;
}
// return result array
return $arr;
}
// Helper function
static function isAssocArray($array, $empty = false)
{
if (empty($array))
return $empty;
$i = 0;
foreach ($array as $key => $value)
{
if ($key !== $i++)
return true;
}
return false;
}
// Helper function
static function isSequentialArray($array, $empty = false)
{
if (empty($array))
return $empty;
$i = 0;
foreach ($array as $key => $value)
{
if ($key !== $i++)
return false;
}
return true;
}
// Helper function
private function setParts($name, &$value)
{
if (empty($value))
{
unset($this->_parts[$name]);
}
else //if ($this->_parts[$name] ?? null !== $value)
{
$this->_parts[$name] = $value;
//$this->_uri = null;
}
}
function __get($name)
{
switch ($name)
{
// properties ordered by lookup frequency
case 'scheme': return $this->_scheme;
case 'host': return $this->_host;
case 'path': return $this->_path;
case 'query': return $this->_query;
case 'port': return $this->_port;
case 'user': return $this->_user;
case 'pass': return $this->_pass;
case 'fragment': return $this->_frag;
//--- End of the common parse_url() properties ---//
//--- Start of extended parse_url() properties ---//
case 'userInfo':
return empty($this->_user) ? null : ($this->_user . (empty($this->_pass) ? null : (':' . $this->_pass)));
case 'segments': // Similar to System.Uri.Segments Property in .NET Framework: https://msdn.microsoft.com/en-us/library/system.uri.segments.aspx
$segments = []; // eg. '/' => ['/'], '/admin/login' => ['/', 'admin/', 'login']
$offset = 0;
while (($pos = strpos($this->_path, '/', $offset)) !== false)
{
$segments[] = substr($this->_path, $offset, $pos - $offset + 1);
$offset = $pos + 1;
}
if (strlen($this->_path) > $offset)
$segments[] = substr($this->_path, $offset);
return $segments;
case 'authority':
if ($this->_host === null)
return null;
$userInfo = $this->userInfo;
return ( $userInfo !== null ? $userInfo . '@' : null) .
$this->_host .
(self::isNonStandardPort($this->_scheme, $this->_host, $this->_port) ? ':' . $this->_port : null);
case 'schemeAndServer': // Idea taken from: https://msdn.microsoft.com/en-us/library/7767559y.aspx
// `The Scheme, Host, and Port data.`
$scheme = $this->_scheme;
if ( ! empty($scheme))
$scheme .= ':';
$host = $this->_host;
if ( ! empty($host))
{
$host = ($this->_scheme === 'mailto' ? null : '//') . $host;
$port = $this->_port;
if ($port !== null)
$port = ':' . $port;
}
return empty($scheme) && empty($host) ? null : ($scheme . $host);
case 'hostAndPort':
if ($this->_host === null)
return null;
$result = $this->_host;
if ($this->_port)
return $this->_host . ($this->_port !== null ? ':' . $this->_port : null);
/*
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.
return $this->_port ?? self::$allowedSchemes[$this->_scheme] ?? -1;
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
// ie. normalize the scheme
// eg. ('Http://' || 'http:') -> 'http'
if (strpos($scheme, ':') !== false)
$scheme = substr($scheme, 0, strpos($scheme, ':'));
$scheme = strtolower($scheme);
// test again - this test is not specifically for lowercase values only, but also for any remaining non-alphanumeric characters
if ( ! ctype_lower($scheme))
{
throw new InvalidArgumentException("Invalid Uri scheme string; received {$value}");
}
}
if ( ! array_key_exists($scheme, self::$allowedSchemes) && ! empty($scheme))
{
throw new InvalidArgumentException("Unsupported Uri scheme `{$scheme}`; must be an empty string or in the set (" . implode(', ', array_keys(self::$allowedSchemes)) . ')');
}
$this->_scheme = empty($scheme) ? null : $scheme;
}
else if ($value === null)
{
$this->_scheme = null;
}
else if ($value instanceof self)
{
$this->_scheme = $value->_scheme;
}
else if (is_array($value) && isset($value[$property]))
{
// this will resend the array value through __set('scheme') again for validation!
$this->scheme = $value[$property];
}
else
{
throw new InvalidArgumentException(
'Invalid Uri scheme string provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'host':
if (is_string($value))
{
$host = &$value;
if ($host[0] === '/')
{
// strip possible prefix characters '//'
$host = ltrim($host, '/');
}
// do other validation here
$this->_host = empty($host) ? null : $host;
}
else if ($value === null)
{
$this->_host = null;
}
else if ($value instanceof self)
{
$this->_host = $value->_host;
}
else if (is_array($value) && isset($value[$property]))
{
// this will resend the array value through __set('host') again for validation!
$this->host = $value[$property];
}
else
{
throw new InvalidArgumentException(
'Invalid Uri host; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'port':
if (is_numeric($value))
{
$port = (int) $value;
if (self::isNonStandardPort($this->_scheme, $this->_host, $port))
{
if ($port < 1 || $port > 65535)
{
throw new InvalidArgumentException(
"Invalid port `{$port}` specified; must be a valid TCP/UDP port"
);
}
}
else
{
$port = null;
}
$this->_port = $port;
}
else if ($value === null)
{
$this->_port = null;
}
else if ($value instanceof self)
{
// resent through validation! Mainly for the non-standard-port checks
$this->port = $value->_port;
}
else if (is_array($value) && isset($value[$property]))
{
// resend value through __set('port') for validation!
$this->port = $value[$property];
}
else
{
throw new InvalidArgumentException(
'Invalid Uri port provided; expecting a numeric string, integer or null; received ' .
(is_object($value) ? get_class($value) : (is_string($value) ? $value : 'value of type ' . gettype($value)))
);
}
return $value;
case 'path':
if (is_string($value))
{
$path = &$value;
if (strpos($path, '?') !== false)
{
throw new InvalidArgumentException(
'Invalid path provided; paths must not contain a query string'
);
}
if (strpos($path, '#') !== false)
{
throw new InvalidArgumentException(
'Invalid path provided; paths must not contain a URI fragment'
);
}
// do other validation here
$path = self::encodePath($path);
$this->_path = empty($path) ? null : $path;
}
else if ($value === null)
{
$this->_path = null;
}
else if ($value instanceof self)
{
$this->_path = $value->_path;
}
else if (is_array($value) && isset($value[$property]))
{
$this->path = $value[$property]; // resend value through __set('path') for validation!
}
else
{
throw new InvalidArgumentException(
'Invalid Uri path; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'query':
if (is_string($value))
{
$query = &$value;
if (empty($query))
{
$this->_query = null;
}
else
{
if (strpos($query, '#') !== false)
throw new InvalidArgumentException(
'Invalid Uri query string provided; query must not contain a URI fragment'
);
$this->_query = $this->encodeQuery($query);
}
}
else if ($value === null)
{
$this->_query = null;
}
else if ($value instanceof self)
{
$this->_query = $value->_query;
}
else if (is_array($value) && isset($value[$property]))
{
$this->query = $value[$property]; // resend value through __set('query') for validation!
}
else
{
throw new InvalidArgumentException(
'Invalid Uri query string provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'fragment':
if (is_string($value))
{
$fragment = &$value;
if ($fragment[0] === '#')
$fragment = substr($fragment, 1);
$this->_frag = $this->encodeFragment($fragment);
}
else if ($value === null)
{
$this->_frag = null;
}
else if ($value instanceof self)
{
$this->_frag = $value->_frag;
}
else if (is_array($value) && isset($value[$property]))
{
// resend value through __set('fragment') for validation!
$this->fragment = $value[$property];
}
else
{
throw new InvalidArgumentException(
'Invalid Uri fragment provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'user':
if ($value === null)
{
$this->_user = null;
}
else if (is_string($value))
{
$user = &$value;
$user = rawurlencode($user);
$this->_user = $user;
}
else if ($value instanceof self)
{
$this->_user = $value->_user;
}
else if (is_array($value) && isset($value[$property]))
{
$this->user = $value[$property]; // resend value through __set('user') for validation!
}
else
{
throw new InvalidArgumentException(
'Invalid username provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'pass':
if ($value === null)
{
$this->_pass = null;
}
else if (is_string($value))
{
$pass = &$value;
$pass = rawurlencode($pass);
$this->_pass = $pass;
}
else if ($value instanceof self)
{
$this->_pass = $value->_pass;
}
else if (is_array($value) && isset($value[$property]))
{
$this->pass = $value[$property]; // resend value through __set('pass') for validation!
}
else
{
throw new InvalidArgumentException(
'Invalid password provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'segments': // Similar to System.Uri.Segments Property in .NET Framework: https://msdn.microsoft.com/en-us/library/system.uri.segments.aspx
// is_array() example; ['/', 'admin/', 'login']
if (is_array($value))
{
$path = (string) implode($value);
$this->_path = empty($path) ? null : $path;
}
else if ($value === null)
{
$this->_path = null;
}
else
{
throw new InvalidArgumentException(
'Invalid segments provided; expecting an array or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'dnssegments':
// ['example', '.com'] => 'example.com'
if (is_array($value))
{
$host = (string) implode($value);
$this->_host = empty($host) ? null : $host;
}
else if ($value === null)
{
$this->_host = null;
}
else
{
throw new InvalidArgumentException(
'Invalid DNS segments; expecting an array or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
case 'authority': // compound property: [user[:pass]@]host[:port]
if (is_string($value) && $value !== '')
{
$authority = &$value;
if ($authority[0] === '/')
$authority = ltrim($authority, '/');
$split = explode('@', $authority);
$count = count($split);
if ($count === 1)
{
$authUser = null;
$authHost = &$split[0];
}
else if ($count === 2)
{
$authUser = &$split[0];
$authHost = &$split[1];
}
else
{
throw new InvalidArgumentException(
'Invalid authority; expecting a string with at most a single `@` sign or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
// detect if optional port exists
if (strpos($authHost, ':') === false)
{
$this->host = $authHost; // run through validation
$this->_port = null;
}
else
{
$pos = strpos($authHost, ':');
$this->host = substr($authHost, 0, $pos);
$this->port = substr($authHost, $pos + 1);
}
if ($authUser === null)
{
$this->_user = null;
$this->_pass = null;
}
else
{
if (strpos($authUser, ':') === false)
{
$this->user = $authUser; // run through validation
$this->_pass = null;
}
else
{
$pos = strpos($authUser, ':');
$this->user = substr($authUser, 0, $pos);
$this->pass = substr($authUser, $pos + 1);
}
}
}
else if ($value === null || $value === '')
{
$this->_user = null;
$this->_pass = null;
$this->_host = null;
$this->_port = null;
}
else if ($value instanceof self)
{
$this->_user = $value->_user;
$this->_pass = $value->_pass;
$this->_host = $value->_host;
$this->_port = $value->_port;
}
else if (is_array($value))
{ // resend all values through __set('...') for validation!
$this->user = $value['user'] ?? null;
$this->pass = $value['pass'] ?? null;
$this->host = $value['host'] ?? null;
$this->port = $value['port'] ?? null;
}
else
{
throw new InvalidArgumentException(
'Invalid authority provided; expecting a string or null; received ' .
(is_object($value) ? get_class($value) : gettype($value))
);
}
return $value;
default:
// if (in_array($name, hash_algos()))
// return hash($name, (string) $this);
$property = &$name;
if ( ! ctype_lower($property))
$property = strtolower($property);
if (self::$hashAlgos === null)
self::$hashAlgos = array_flip(hash_algos());
if (isset(self::$hashAlgos[$property]))
{
throw new InvalidArgumentException(
"Invalid Uri property `{$name}`; hash properties are read-only"
);
}
//--- Start of alias and mixed-case properties ---//
switch ($property)
{
// possible mixed-case variants `normalized` to lowercase. eg. `Scheme` => `scheme`
case 'scheme': return $this->scheme = $value;
case 'host': return $this->host = $value;
case 'path': return $this->path = $value;
case 'query': return $this->query = $value;
case 'user': return $this->user = $value;
case 'pass': return $this->pass = $value;
case 'port': return $this->port = $value;
case 'fragment': return $this->fragment = $value;
case 'realport': // My alias of .NET `Uri.StrongPort`
case 'strongport':
// This is called the Uri.StrongPort property in .NET
// `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.`
return $this->port ?? self::$allowedSchemes[$this->scheme] ?? -1;
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);
}
}