Rev 79 | Rev 85 | Go to most recent revision | Blame | Compare with Previous | Last modification | View Log | RSS feed
<?php
// +-----------------------------------------------------------------------+
// | Copyright (c) 2007-2008, Christian Schmidt, Peytz & Co. A/S |
// | All rights reserved. |
// | |
// | Redistribution and use in source and binary forms, with or without |
// | modification, are permitted provided that the following conditions |
// | are met: |
// | |
// | o Redistributions of source code must retain the above copyright |
// | notice, this list of conditions and the following disclaimer. |
// | o Redistributions in binary form must reproduce the above copyright |
// | notice, this list of conditions and the following disclaimer in the |
// | documentation and/or other materials provided with the distribution.|
// | o The names of the authors may not be used to endorse or promote |
// | products derived from this software without specific prior written |
// | permission. |
// | |
// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
// | |
// +-----------------------------------------------------------------------+
// | Author: Christian Schmidt <schmidt at php dot net> |
// +-----------------------------------------------------------------------+
//
// $Id: Net_URL2.php 80 2009-08-27 07:30:42Z jpm $
//
// Net_URL2 Class (PHP5 Only)
// This code is released under the BSD License - http://www.opensource.org/licenses/bsd-license.php
/**
* @license BSD License
*/
class Net_URL2
{
/**
* Do strict parsing in resolve() (see RFC 3986, section 5.2.2). Default
* is true.
*/
const OPTION_STRICT = 'strict';
/**
* Represent arrays in query using PHP's [] notation. Default is true.
*/
const OPTION_USE_BRACKETS = 'use_brackets';
/**
* URL-encode query variable keys. Default is true.
*/
const OPTION_ENCODE_KEYS = 'encode_keys';
/**
* Query variable separators when parsing the query string. Every character
* is considered a separator. Default is specified by the
* arg_separator.input php.ini setting (this defaults to "&").
*/
const OPTION_SEPARATOR_INPUT = 'input_separator';
/**
* Query variable separator used when generating the query string. Default
* is specified by the arg_separator.output php.ini setting (this defaults
* to "&").
*/
const OPTION_SEPARATOR_OUTPUT = 'output_separator';
/**
* Default options corresponds to how PHP handles $_GET.
*/
private $options = array(
self::OPTION_STRICT => true,
self::OPTION_USE_BRACKETS => true,
self::OPTION_ENCODE_KEYS => true,
self::OPTION_SEPARATOR_INPUT => 'x&',
self::OPTION_SEPARATOR_OUTPUT => 'x&',
);
/**
* @var string|bool
*/
private $scheme = false;
/**
* @var string|bool
*/
private $userinfo = false;
/**
* @var string|bool
*/
private $host = false;
/**
* @var int|bool
*/
private $port = false;
/**
* @var string
*/
private $path = '';
/**
* @var string|bool
*/
private $query = false;
/**
* @var string|bool
*/
private $fragment = false;
/**
* @param string $url an absolute or relative URL
* @param array $options
*/
public function __construct($url, $options = null)
{
$this->setOption(self::OPTION_SEPARATOR_INPUT,
ini_get('arg_separator.input'));
$this->setOption(self::OPTION_SEPARATOR_OUTPUT,
ini_get('arg_separator.output'));
if (is_array($options)) {
foreach ($options as $optionName => $value) {
$this->setOption($optionName);
}
}
if (preg_match('@^([a-z][a-z0-9.+-]*):@i', $url, $reg)) {
$this->scheme = $reg[1];
$url = substr($url, strlen($reg[0]));
}
if (preg_match('@^//([^/#?]+)@', $url, $reg)) {
$this->setAuthority($reg[1]);
$url = substr($url, strlen($reg[0]));
}
$i = strcspn($url, '?#');
$this->path = substr($url, 0, $i);
$url = substr($url, $i);
if (preg_match('@^\?([^#]*)@', $url, $reg)) {
$this->query = $reg[1];
$url = substr($url, strlen($reg[0]));
}
if ($url) {
$this->fragment = substr($url, 1);
}
}
/**
* Returns the scheme, e.g. "http" or "urn", or false if there is no
* scheme specified, i.e. if this is a relative URL.
*
* @return string|bool
*/
public function getScheme()
{
return $this->scheme;
}
/**
* @param string|bool $scheme
*
* @return void
* @see getScheme()
*/
public function setScheme($scheme)
{
$this->scheme = $scheme;
}
/**
* Returns the user part of the userinfo part (the part preceding the first
* ":"), or false if there is no userinfo part.
*
* @return string|bool
*/
public function getUser()
{
return $this->userinfo !== false ? preg_replace('@:.*$@', '', $this->userinfo) : false;
}
/**
* Returns the password part of the userinfo part (the part after the first
* ":"), or false if there is no userinfo part (i.e. the URL does not
* contain "@" in front of the hostname) or the userinfo part does not
* contain ":".
*
* @return string|bool
*/
public function getPassword()
{
return $this->userinfo !== false ? substr(strstr($this->userinfo, ':'), 1) : false;
}
/**
* Returns the userinfo part, or false if there is none, i.e. if the
* authority part does not contain "@".
*
* @return string|bool
*/
public function getUserinfo()
{
return $this->userinfo;
}
/**
* Sets the userinfo part. If two arguments are passed, they are combined
* in the userinfo part as username ":" password.
*
* @param string|bool $userinfo userinfo or username
* @param string|bool $password
*
* @return void
*/
public function setUserinfo($userinfo, $password = false)
{
$this->userinfo = $userinfo;
if ($password !== false) {
$this->userinfo .= ':' . $password;
}
}
/**
* Returns the host part, or false if there is no authority part, e.g.
* relative URLs.
*
* @return string|bool
*/
public function getHost()
{
return $this->host;
}
/**
* @param string|bool $host
*
* @return void
*/
public function setHost($host)
{
$this->host = $host;
}
/**
* Returns the port number, or false if there is no port number specified,
* i.e. if the default port is to be used.
*
* @return int|bool
*/
public function getPort()
{
return $this->port;
}
/**
* @param int|bool $port
*
* @return void
*/
public function setPort($port)
{
$this->port = intval($port);
}
/**
* Returns the authority part, i.e. [ userinfo "@" ] host [ ":" port ], or
* false if there is no authority none.
*
* @return string|bool
*/
public function getAuthority()
{
if (!$this->host) {
return false;
}
$authority = '';
if ($this->userinfo !== false) {
$authority .= $this->userinfo . '@';
}
$authority .= $this->host;
if ($this->port !== false) {
$authority .= ':' . $this->port;
}
return $authority;
}
/**
* @param string|false $authority
*
* @return void
*/
public function setAuthority($authority)
{
$this->user = false;
$this->pass = false;
$this->host = false;
$this->port = false;
if (preg_match('@^(([^\@]+)\@)?([^:]+)(:(\d*))?$@', $authority, $reg)) {
if ($reg[1]) {
$this->userinfo = $reg[2];
}
$this->host = $reg[3];
if (isset($reg[5])) {
$this->port = intval($reg[5]);
}
}
}
/**
* Returns the path part (possibly an empty string).
*
* @return string
*/
public function getPath()
{
return $this->path;
}
/**
* @param string $path
*
* @return void
*/
public function setPath($path)
{
$this->path = $path;
}
/**
* Returns the query string (excluding the leading "?"), or false if "?"
* isn't present in the URL.
*
* @return string|bool
* @see self::getQueryVariables()
*/
public function getQuery()
{
return $this->query;
}
/**
* @param string|bool $query
*
* @return void
* @see self::setQueryVariables()
*/
public function setQuery($query)
{
$this->query = $query;
}
/**
* Returns the fragment name, or false if "#" isn't present in the URL.
*
* @return string|bool
*/
public function getFragment()
{
return $this->fragment;
}
/**
* @param string|bool $fragment
*
* @return void
*/
public function setFragment($fragment)
{
$this->fragment = $fragment;
}
/**
* Returns the query string like an array as the variables would appear in
* $_GET in a PHP script.
*
* @return array
*/
public function getQueryVariables()
{
$pattern = '/[' .
preg_quote($this->getOption(self::OPTION_SEPARATOR_INPUT), '/') .
']/';
$parts = preg_split($pattern, $this->query, -1, PREG_SPLIT_NO_EMPTY);
$return = array();
foreach ($parts as $part) {
if (strpos($part, '=') !== false) {
list($key, $value) = explode('=', $part, 2);
} else {
$key = $part;
$value = null;
}
if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
$key = rawurldecode($key);
}
$value = rawurldecode($value);
if ($this->getOption(self::OPTION_USE_BRACKETS) &&
preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) {
$key = $matches[1];
$idx = $matches[2];
// Ensure is an array
if (empty($return[$key]) || !is_array($return[$key])) {
$return[$key] = array();
}
// Add data
if ($idx === '') {
$return[$key][] = $value;
} else {
$return[$key][$idx] = $value;
}
} elseif (!$this->getOption(self::OPTION_USE_BRACKETS)
&& !empty($return[$key])
) {
$return[$key] = (array) $return[$key];
$return[$key][] = $value;
} else {
$return[$key] = $value;
}
}
return $return;
}
/**
* @param array $array (name => value) array
*
* @return void
*/
public function setQueryVariables(array $array)
{
if (!$array) {
$this->query = false;
} else {
foreach ($array as $name => $value) {
if ($this->getOption(self::OPTION_ENCODE_KEYS)) {
$name = rawurlencode($name);
}
if (is_array($value)) {
foreach ($value as $k => $v) {
$parts[] = $this->getOption(self::OPTION_USE_BRACKETS)
? sprintf('%s[%s]=%s', $name, $k, $v)
: ($name . '=' . $v);
}
} elseif (!is_null($value)) {
$parts[] = $name . '=' . $value;
} else {
$parts[] = $name;
}
}
$this->query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT),
$parts);
}
}
/**
* @param string $name
* @param mixed $value
*
* @return array
*/
public function setQueryVariable($name, $value)
{
$array = $this->getQueryVariables();
$array[$name] = $value;
$this->setQueryVariables($array);
}
/**
* @param string $name
*
* @return void
*/
public function unsetQueryVariable($name)
{
$array = $this->getQueryVariables();
unset($array[$name]);
$this->setQueryVariables($array);
}
/**
* Returns a string representation of this URL.
*
* @return string
*/
public function getURL()
{
// See RFC 3986, section 5.3
$url = "";
if ($this->scheme !== false) {
$url .= $this->scheme . ':';
}
$authority = $this->getAuthority();
if ($authority !== false) {
$url .= '//' . $authority;
}
$url .= $this->path;
if ($this->query !== false) {
$url .= '?' . $this->query;
}
if ($this->fragment !== false) {
$url .= '#' . $this->fragment;
}
return $url;
}
/**
* Returns a normalized string representation of this URL. This is useful
* for comparison of URLs.
*
* @return string
*/
public function getNormalizedURL()
{
$url = clone $this;
$url->normalize();
return $url->getUrl();
}
/**
* Returns a normalized Net_URL2 instance.
*
* @return Net_URL2
*/
public function normalize()
{
// See RFC 3886, section 6
// Schemes are case-insensitive
if ($this->scheme) {
$this->scheme = strtolower($this->scheme);
}
// Hostnames are case-insensitive
if ($this->host) {
$this->host = strtolower($this->host);
}
// Remove default port number for known schemes (RFC 3986, section 6.2.3)
if ($this->port &&
$this->scheme &&
$this->port == getservbyname($this->scheme, 'tcp')) {
$this->port = false;
}
// Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1)
foreach (array('userinfo', 'host', 'path') as $part) {
if ($this->$part) {
$this->$part = preg_replace('/%[0-9a-f]{2}/ie', 'strtoupper("\0")', $this->$part);
}
}
// Path segment normalization (RFC 3986, section 6.2.2.3)
$this->path = self::removeDotSegments($this->path);
// Scheme based normalization (RFC 3986, section 6.2.3)
if ($this->host && !$this->path) {
$this->path = '/';
}
}
/**
* Returns whether this instance represents an absolute URL.
*
* @return bool
*/
public function isAbsolute()
{
return (bool) $this->scheme;
}
/**
* Returns an Net_URL2 instance representing an absolute URL relative to
* this URL.
*
* @param Net_URL2|string $reference relative URL
*
* @return Net_URL2
*/
public function resolve($reference)
{
if (is_string($reference)) {
$reference = new self($reference);
}
if (!$this->isAbsolute()) {
throw new Exception('Base-URL must be absolute');
}
// A non-strict parser may ignore a scheme in the reference if it is
// identical to the base URI's scheme.
if (!$this->getOption(self::OPTION_STRICT) && $reference->scheme == $this->scheme) {
$reference->scheme = false;
}
$target = new self('');
if ($reference->scheme !== false) {
$target->scheme = $reference->scheme;
$target->setAuthority($reference->getAuthority());
$target->path = self::removeDotSegments($reference->path);
$target->query = $reference->query;
} else {
$authority = $reference->getAuthority();
if ($authority !== false) {
$target->setAuthority($authority);
$target->path = self::removeDotSegments($reference->path);
$target->query = $reference->query;
} else {
if ($reference->path == '') {
$target->path = $this->path;
if ($reference->query !== false) {
$target->query = $reference->query;
} else {
$target->query = $this->query;
}
} else {
if (substr($reference->path, 0, 1) == '/') {
$target->path = self::removeDotSegments($reference->path);
} else {
// Merge paths (RFC 3986, section 5.2.3)
if ($this->host !== false && $this->path == '') {
$target->path = '/' . $this->path;
} else {
$i = strrpos($this->path, '/');
if ($i !== false) {
$target->path = substr($this->path, 0, $i + 1);
}
$target->path .= $reference->path;
}
$target->path = self::removeDotSegments($target->path);
}
$target->query = $reference->query;
}
$target->setAuthority($this->getAuthority());
}
$target->scheme = $this->scheme;
}
$target->fragment = $reference->fragment;
return $target;
}
/**
* Removes dots as described in RFC 3986, section 5.2.4, e.g.
* "/foo/../bar/baz" => "/bar/baz"
*
* @param string $path a path
*
* @return string a path
*/
private static function removeDotSegments($path)
{
$output = '';
// Make sure not to be trapped in an infinite loop due to a bug in this
// method
$j = 0;
while ($path && $j++ < 100) {
// Step A
if (substr($path, 0, 2) == './') {
$path = substr($path, 2);
} elseif (substr($path, 0, 3) == '../') {
$path = substr($path, 3);
// Step B
} elseif (substr($path, 0, 3) == '/./' || $path == '/.') {
$path = '/' . substr($path, 3);
// Step C
} elseif (substr($path, 0, 4) == '/../' || $path == '/..') {
$path = '/' . substr($path, 4);
$i = strrpos($output, '/');
$output = $i === false ? '' : substr($output, 0, $i);
// Step D
} elseif ($path == '.' || $path == '..') {
$path = '';
// Step E
} else {
$i = strpos($path, '/');
if ($i === 0) {
$i = strpos($path, '/', 1);
}
if ($i === false) {
$i = strlen($path);
}
$output .= substr($path, 0, $i);
$path = substr($path, $i);
}
}
return $output;
}
/**
* Returns a Net_URL2 instance representing the canonical URL of the
* currently executing PHP script.
*
* @return string
*/
public static function getCanonical()
{
if (!isset($_SERVER['REQUEST_METHOD'])) {
// ALERT - no current URL
throw new Exception('Script was not called through a webserver');
}
// Begin with a relative URL
$url = new self($_SERVER['PHP_SELF']);
$url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
$url->host = $_SERVER['SERVER_NAME'];
$port = intval($_SERVER['SERVER_PORT']);
if ($url->scheme == 'http' && $port != 80 ||
$url->scheme == 'https' && $port != 443) {
$url->port = $port;
}
return $url;
}
/**
* Returns the URL used to retrieve the current request.
*
* @return string
*/
public static function getRequestedURL()
{
return self::getRequested()->getUrl();
}
/**
* Returns a Net_URL2 instance representing the URL used to retrieve the
* current request.
*
* @return Net_URL2
*/
public static function getRequested()
{
if (!isset($_SERVER['REQUEST_METHOD'])) {
// ALERT - no current URL
throw new Exception('Script was not called through a webserver');
}
// Begin with a relative URL
$url = new self($_SERVER['REQUEST_URI']);
$url->scheme = isset($_SERVER['HTTPS']) ? 'https' : 'http';
// Set host and possibly port
$url->setAuthority($_SERVER['HTTP_HOST']);
return $url;
}
/**
* Sets the specified option.
*
* @param string $optionName a self::OPTION_ constant
* @param mixed $value option value
*
* @return void
* @see self::OPTION_STRICT
* @see self::OPTION_USE_BRACKETS
* @see self::OPTION_ENCODE_KEYS
*/
function setOption($optionName, $value)
{
if (!array_key_exists($optionName, $this->options)) {
return false;
}
$this->options[$optionName] = $value;
}
/**
* Returns the value of the specified option.
*
* @param string $optionName The name of the option to retrieve
*
* @return mixed
*/
function getOption($optionName)
{
return isset($this->options[$optionName])
? $this->options[$optionName] : false;
}
public function __toString() {
return $this->getURL();
}
}