X-Git-Url: https://git.mxchange.org/?a=blobdiff_plain;f=extlib%2FNet%2FURL2.php;h=1d2f7fa6a4dcfdbbf170e5a360e64204ecf4f9e9;hb=c285f80b1830cffd20a28c693d74c59f8c3c39f6;hp=f7fbcd9ce76ef21ba4243eff055d224ee0129d08;hpb=8884a5255fb90fda67b63fa0d4252d77176337e5;p=quix0rs-gnu-social.git diff --git a/extlib/Net/URL2.php b/extlib/Net/URL2.php index f7fbcd9ce7..1d2f7fa6a4 100644 --- a/extlib/Net/URL2.php +++ b/extlib/Net/URL2.php @@ -18,9 +18,9 @@ * * 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. - * * Neither the name of the PHP_LexerGenerator nor the names of its - * contributors may be used to endorse or promote products derived - * from this software without specific prior written permission. + * * Neither the name of the Net_URL2 nor the names of its contributors may + * 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, @@ -36,11 +36,11 @@ * * @category Networking * @package Net_URL2 - * @author Christian Schmidt - * @copyright 2007-2008 Peytz & Co. A/S - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version CVS: $Id: URL2.php 286661 2009-08-02 12:50:54Z schmidt $ - * @link http://www.rfc-editor.org/rfc/rfc3986.txt + * @author Christian Schmidt + * @copyright 2007-2009 Peytz & Co. A/S + * @license https://spdx.org/licenses/BSD-3-Clause BSD-3-Clause + * @version CVS: $Id$ + * @link https://tools.ietf.org/html/rfc3986 */ /** @@ -48,11 +48,11 @@ * * @category Networking * @package Net_URL2 - * @author Christian Schmidt - * @copyright 2007-2008 Peytz & Co. ApS - * @license http://www.opensource.org/licenses/bsd-license.php New BSD License - * @version Release: @package_version@ - * @link http://pear.php.net/package/Net_URL2 + * @author Christian Schmidt + * @copyright 2007-2009 Peytz & Co. A/S + * @license https://spdx.org/licenses/BSD-3-Clause BSD-3-Clause + * @version Release: 2.1.2 + * @link https://pear.php.net/package/Net_URL2 */ class Net_URL2 { @@ -67,6 +67,12 @@ class Net_URL2 */ const OPTION_USE_BRACKETS = 'use_brackets'; + /** + * Drop zero-based integer sequences in query using PHP's [] notation. Default + * is true. + */ + const OPTION_DROP_SEQUENCE = 'drop_sequence'; + /** * URL-encode query variable keys. Default is true. */ @@ -74,15 +80,13 @@ class Net_URL2 /** * 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 "&"). + * is considered a separator. Default is "&". */ 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 "&"). + * is "&". */ const OPTION_SEPARATOR_OUTPUT = 'output_separator'; @@ -92,9 +96,10 @@ class Net_URL2 private $_options = array( self::OPTION_STRICT => true, self::OPTION_USE_BRACKETS => true, + self::OPTION_DROP_SEQUENCE => true, self::OPTION_ENCODE_KEYS => true, - self::OPTION_SEPARATOR_INPUT => 'x&', - self::OPTION_SEPARATOR_OUTPUT => 'x&', + self::OPTION_SEPARATOR_INPUT => '&', + self::OPTION_SEPARATOR_OUTPUT => '&', ); /** @@ -113,7 +118,7 @@ class Net_URL2 private $_host = false; /** - * @var int|bool + * @var string|bool */ private $_port = false; @@ -137,41 +142,18 @@ class Net_URL2 * * @param string $url an absolute or relative URL * @param array $options an array of OPTION_xxx constants + * + * @uses self::parseUrl() */ - public function __construct($url, $options = null) + public function __construct($url, array $options = array()) { - $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, $value); + foreach ($options as $optionName => $value) { + if (array_key_exists($optionName, $this->_options)) { + $this->_options[$optionName] = $value; } } - 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); - } + $this->parseUrl($url); } /** @@ -180,8 +162,9 @@ class Net_URL2 * This method will magically set the value of a private variable ($var) * with the value passed as the args * - * @param string $var The private variable to set. - * @param mixed $arg An argument of any type. + * @param string $var The private variable to set. + * @param mixed $arg An argument of any type. + * * @return void */ public function __set($var, $arg) @@ -191,17 +174,18 @@ class Net_URL2 $this->$method($arg); } } - + /** * Magic Getter. * - * This is the magic get method to retrieve the private variable + * This is the magic get method to retrieve the private variable * that was set by either __set() or it's setter... - * - * @param string $var The property name to retrieve. - * @return mixed $this->$var Either a boolean false if the - * property is not set or the value - * of the private property. + * + * @param string $var The property name to retrieve. + * + * @return mixed $this->$var Either a boolean false if the + * property is not set or the value + * of the private property. */ public function __get($var) { @@ -209,15 +193,15 @@ class Net_URL2 if (method_exists($this, $method)) { return $this->$method(); } - + return false; } - + /** * 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 + * @return string|bool */ public function getScheme() { @@ -232,24 +216,25 @@ class Net_URL2 * scheme specified, i.e. if this is a relative * URL * - * @return void - * @see getScheme() + * @return $this + * @see getScheme */ public function setScheme($scheme) { $this->_scheme = $scheme; + return $this; } /** * Returns the user part of the userinfo part (the part preceding the first * ":"), or false if there is no userinfo part. * - * @return string|bool + * @return string|bool */ public function getUser() { return $this->_userinfo !== false - ? preg_replace('@:.*$@', '', $this->_userinfo) + ? preg_replace('(:.*$)', '', $this->_userinfo) : false; } @@ -259,7 +244,7 @@ class Net_URL2 * contain "@" in front of the hostname) or the userinfo part does not * contain ":". * - * @return string|bool + * @return string|bool */ public function getPassword() { @@ -272,7 +257,7 @@ class Net_URL2 * Returns the userinfo part, or false if there is none, i.e. if the * authority part does not contain "@". * - * @return string|bool + * @return string|bool */ public function getUserinfo() { @@ -286,21 +271,27 @@ class Net_URL2 * @param string|bool $userinfo userinfo or username * @param string|bool $password optional password, or false * - * @return void + * @return $this */ public function setUserinfo($userinfo, $password = false) { - $this->_userinfo = $userinfo; if ($password !== false) { - $this->_userinfo .= ':' . $password; + $userinfo .= ':' . $password; } + + if ($userinfo !== false) { + $userinfo = $this->_encodeData($userinfo); + } + + $this->_userinfo = $userinfo; + return $this; } /** * Returns the host part, or false if there is no authority part, e.g. * relative URLs. * - * @return string|bool a hostname, an IP address, or false + * @return string|bool a hostname, an IP address, or false */ public function getHost() { @@ -313,18 +304,19 @@ class Net_URL2 * * @param string|bool $host a hostname, an IP address, or false * - * @return void + * @return $this */ public function setHost($host) { $this->_host = $host; + return $this; } /** * 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 + * @return string|bool */ public function getPort() { @@ -335,13 +327,14 @@ class Net_URL2 * Sets the port number. Specify false if there is no port number specified, * i.e. if the default port is to be used. * - * @param int|bool $port a port number, or false + * @param string|bool $port a port number, or false * - * @return void + * @return $this */ public function setPort($port) { - $this->_port = intval($port); + $this->_port = $port; + return $this; } /** @@ -352,13 +345,13 @@ class Net_URL2 */ public function getAuthority() { - if (!$this->_host) { + if (false === $this->_host) { return false; } $authority = ''; - if ($this->_userinfo !== false) { + if (strlen($this->_userinfo)) { $authority .= $this->_userinfo . '@'; } @@ -375,27 +368,37 @@ class Net_URL2 * Sets the authority part, i.e. [ userinfo "@" ] host [ ":" port ]. Specify * false if there is no authority. * - * @param string|false $authority a hostname or an IP addresse, possibly + * @param string|bool $authority a hostname or an IP address, possibly * with userinfo prefixed and port number * appended, e.g. "foo:bar@example.org:81". * - * @return void + * @return $this */ public function setAuthority($authority) { $this->_userinfo = 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]); - } + if ('' === $authority) { + $this->_host = $authority; + return $this; } + + if (!preg_match('(^(([^@]*)@)?(.+?)(:(\d*))?$)', $authority, $matches)) { + return $this; + } + + if ($matches[1]) { + $this->_userinfo = $this->_encodeData($matches[2]); + } + + $this->_host = $matches[3]; + + if (isset($matches[5]) && strlen($matches[5])) { + $this->_port = $matches[5]; + } + return $this; } /** @@ -413,11 +416,12 @@ class Net_URL2 * * @param string $path a path * - * @return void + * @return $this */ public function setPath($path) { $this->_path = $path; + return $this; } /** @@ -425,7 +429,7 @@ class Net_URL2 * is not present in the URL. * * @return string|bool - * @see self::getQueryVariables() + * @see getQueryVariables */ public function getQuery() { @@ -438,18 +442,19 @@ class Net_URL2 * * @param string|bool $query a query string, e.g. "foo=1&bar=2" * - * @return void - * @see self::setQueryVariables() + * @return $this + * @see setQueryVariables */ public function setQuery($query) { $this->_query = $query; + return $this; } /** * Returns the fragment name, or false if "#" is not present in the URL. * - * @return string|bool + * @return string|bool */ public function getFragment() { @@ -462,11 +467,12 @@ class Net_URL2 * @param string|bool $fragment a fragment excluding the leading "#", or * false * - * @return void + * @return $this */ public function setFragment($fragment) { $this->_fragment = $fragment; + return $this; } /** @@ -474,57 +480,165 @@ class Net_URL2 * $_GET in a PHP script. If the URL does not contain a "?", an empty array * is returned. * - * @return array + * @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); + $separator = $this->getOption(self::OPTION_SEPARATOR_INPUT); + $encodeKeys = $this->getOption(self::OPTION_ENCODE_KEYS); + $useBrackets = $this->getOption(self::OPTION_USE_BRACKETS); + $return = array(); - foreach ($parts as $part) { - if (strpos($part, '=') !== false) { - list($key, $value) = explode('=', $part, 2); - } else { - $key = $part; - $value = null; - } + for ($part = strtok($this->_query, $separator); + strlen($part); + $part = strtok($separator) + ) { + list($key, $value) = explode('=', $part, 2) + array(1 => ''); - if ($this->getOption(self::OPTION_ENCODE_KEYS)) { + if ($encodeKeys) { $key = rawurldecode($key); } $value = rawurldecode($value); - if ($this->getOption(self::OPTION_USE_BRACKETS) && - preg_match('#^(.*)\[([0-9a-z_-]*)\]#i', $key, $matches)) { + if ($useBrackets) { + $return = $this->_queryArrayByKey($key, $value, $return); + } else { + if (isset($return[$key])) { + $return[$key] = (array) $return[$key]; + $return[$key][] = $value; + } else { + $return[$key] = $value; + } + } + } - $key = $matches[1]; - $idx = $matches[2]; + return $return; + } - // Ensure is an array - if (empty($return[$key]) || !is_array($return[$key])) { - $return[$key] = array(); - } + /** + * Parse a single query key=value pair into an existing php array + * + * @param string $key query-key + * @param string $value query-value + * @param array $array of existing query variables (if any) + * + * @return mixed + */ + private function _queryArrayByKey($key, $value, array $array = array()) + { + if (!strlen($key)) { + return $array; + } - // Add data - if ($idx === '') { - $return[$key][] = $value; - } else { - $return[$key][$idx] = $value; + $offset = $this->_queryKeyBracketOffset($key); + if ($offset === false) { + $name = $key; + } else { + $name = substr($key, 0, $offset); + } + + if (!strlen($name)) { + return $array; + } + + if (!$offset) { + // named value + $array[$name] = $value; + } else { + // array + $brackets = substr($key, $offset); + if (!isset($array[$name])) { + $array[$name] = null; + } + $array[$name] = $this->_queryArrayByBrackets( + $brackets, $value, $array[$name] + ); + } + + return $array; + } + + /** + * Parse a key-buffer to place value in array + * + * @param string $buffer to consume all keys from + * @param string $value to be set/add + * @param array $array to traverse and set/add value in + * + * @throws Exception + * @return array + */ + private function _queryArrayByBrackets($buffer, $value, array $array = null) + { + $entry = &$array; + + for ($iteration = 0; strlen($buffer); $iteration++) { + $open = $this->_queryKeyBracketOffset($buffer); + if ($open !== 0) { + // Opening bracket [ must exist at offset 0, if not, there is + // no bracket to parse and the value dropped. + // if this happens in the first iteration, this is flawed, see + // as well the second exception below. + if ($iteration) { + break; } - } elseif (!$this->getOption(self::OPTION_USE_BRACKETS) - && !empty($return[$key]) - ) { - $return[$key] = (array) $return[$key]; - $return[$key][] = $value; + // @codeCoverageIgnoreStart + throw new Exception( + 'Net_URL2 Internal Error: '. __METHOD__ .'(): ' . + 'Opening bracket [ must exist at offset 0' + ); + // @codeCoverageIgnoreEnd + } + + $close = strpos($buffer, ']', 1); + if (!$close) { + // this error condition should never be reached as this is a + // private method and bracket pairs are checked beforehand. + // See as well the first exception for the opening bracket. + // @codeCoverageIgnoreStart + throw new Exception( + 'Net_URL2 Internal Error: '. __METHOD__ .'(): ' . + 'Closing bracket ] must exist, not found' + ); + // @codeCoverageIgnoreEnd + } + + $index = substr($buffer, 1, $close - 1); + if (strlen($index)) { + $entry = &$entry[$index]; } else { - $return[$key] = $value; + if (!is_array($entry)) { + $entry = array(); + } + $entry[] = &$new; + $entry = &$new; + unset($new); } + $buffer = substr($buffer, $close + 1); } - return $return; + $entry = $value; + + return $array; + } + + /** + * Query-key has brackets ("...[]") + * + * @param string $key query-key + * + * @return bool|int offset of opening bracket, false if no brackets + */ + private function _queryKeyBracketOffset($key) + { + if (false !== $open = strpos($key, '[') + and false === strpos($key, ']', $open + 1) + ) { + $open = false; + } + + return $open; } /** @@ -532,33 +646,19 @@ class Net_URL2 * * @param array $array (name => value) array * - * @return void + * @return $this */ public function setQueryVariables(array $array) { if (!$array) { $this->_query = false; } else { - foreach ($array as $name => $value) { - if ($this->getOption(self::OPTION_ENCODE_KEYS)) { - $name = self::urlencode($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 . '=' . self::urlencode($value); - } else { - $parts[] = $name; - } - } - $this->_query = implode($this->getOption(self::OPTION_SEPARATOR_OUTPUT), - $parts); + $this->_query = $this->buildQuery( + $array, + $this->getOption(self::OPTION_SEPARATOR_OUTPUT) + ); } + return $this; } /** @@ -567,17 +667,18 @@ class Net_URL2 * @param string $name variable name * @param mixed $value variable value * - * @return array + * @return $this */ public function setQueryVariable($name, $value) { $array = $this->getQueryVariables(); $array[$name] = $value; $this->setQueryVariables($array); + return $this; } /** - * Removes the specifed variable from the query string. + * Removes the specified variable from the query string. * * @param string $name a query string variable, e.g. "foo" in "?foo=1" * @@ -593,22 +694,23 @@ class Net_URL2 /** * Returns a string representation of this URL. * - * @return string + * @return string */ public function getURL() { // See RFC 3986, section 5.3 - $url = ""; + $url = ''; if ($this->_scheme !== false) { $url .= $this->_scheme . ':'; } $authority = $this->getAuthority(); - if ($authority !== false) { - $url .= '//' . $authority; + if ($authority === false && strtolower($this->_scheme) === 'file') { + $authority = ''; } - $url .= $this->_path; + + $url .= $this->_buildAuthorityAndPath($authority, $this->_path); if ($this->_query !== false) { $url .= '?' . $this->_query; @@ -617,83 +719,151 @@ class Net_URL2 if ($this->_fragment !== false) { $url .= '#' . $this->_fragment; } - + return $url; } + /** + * Put authority and path together, wrapping authority + * into proper separators/terminators. + * + * @param string|bool $authority authority + * @param string $path path + * + * @return string + */ + private function _buildAuthorityAndPath($authority, $path) + { + if ($authority === false) { + return $path; + } + + $terminator = ($path !== '' && $path[0] !== '/') ? '/' : ''; + + return '//' . $authority . $terminator . $path; + } + /** * Returns a string representation of this URL. * - * @return string - * @see toString() + * @return string + * @link https://php.net/language.oop5.magic#object.tostring */ public function __toString() { return $this->getURL(); } - /** + /** * Returns a normalized string representation of this URL. This is useful * for comparison of URLs. * - * @return string + * @return string */ public function getNormalizedURL() { $url = clone $this; $url->normalize(); - return $url->getUrl(); + return $url->getURL(); } - /** - * Returns a normalized Net_URL2 instance. + /** + * Normalizes the URL + * + * See RFC 3986, Section 6. Normalization and Comparison + * + * @link https://tools.ietf.org/html/rfc3986#section-6 * - * @return Net_URL2 + * @return void */ public function normalize() { - // See RFC 3886, section 6 + // See RFC 3986, section 6 - // Schemes are case-insensitive + // Scheme is case-insensitive if ($this->_scheme) { $this->_scheme = strtolower($this->_scheme); } - // Hostnames are case-insensitive + // Hostname is 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')) { - + if ('' === $this->_port + || $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); + // Normalize percentage-encoded unreserved characters (section 6.2.2.2) + $fields = array(&$this->_userinfo, &$this->_host, &$this->_path, + &$this->_query, &$this->_fragment); + foreach ($fields as &$field) { + if ($field !== false) { + $field = $this->_normalize("$field"); } } + unset($field); // 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) { + if (false !== $this->_host && '' === $this->_path) { $this->_path = '/'; } + + // path should start with '/' if there is authority (section 3.3.) + if (strlen($this->getAuthority()) + && strlen($this->_path) + && $this->_path[0] !== '/' + ) { + $this->_path = '/' . $this->_path; + } + } + + /** + * Normalize case of %XX percentage-encodings (RFC 3986, section 6.2.2.1) + * Normalize percentage-encoded unreserved characters (section 6.2.2.2) + * + * @param string|array $mixed string or array of strings to normalize + * + * @return string|array + * @see normalize + * @see _normalizeCallback() + */ + private function _normalize($mixed) + { + return preg_replace_callback( + '((?:%[0-9a-fA-Z]{2})+)', array($this, '_normalizeCallback'), + $mixed + ); + } + + /** + * Callback for _normalize() of %XX percentage-encodings + * + * @param array $matches as by preg_replace_callback + * + * @return string + * @see normalize + * @see _normalize + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function _normalizeCallback($matches) + { + return self::urlencode(urldecode($matches[0])); } /** * Returns whether this instance represents an absolute URL. * - * @return bool + * @return bool */ public function isAbsolute() { @@ -706,20 +876,25 @@ class Net_URL2 * * @param Net_URL2|string $reference relative URL * - * @return Net_URL2 + * @throws Exception + * @return $this */ public function resolve($reference) { if (!$reference instanceof Net_URL2) { $reference = new self($reference); } - if (!$this->isAbsolute()) { - throw new Exception('Base-URL must be absolute'); + if (!$reference->_isFragmentOnly() && !$this->isAbsolute()) { + throw new Exception( + 'Base-URL must be absolute if reference is not fragment-only' + ); } // 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) { + if (!$this->getOption(self::OPTION_STRICT) + && $reference->_scheme == $this->_scheme + ) { $reference->_scheme = false; } @@ -749,7 +924,7 @@ class Net_URL2 } else { // Merge paths (RFC 3986, section 5.2.3) if ($this->_host !== false && $this->_path == '') { - $target->_path = '/' . $this->_path; + $target->_path = '/' . $reference->_path; } else { $i = strrpos($this->_path, '/'); if ($i !== false) { @@ -771,6 +946,25 @@ class Net_URL2 return $target; } + /** + * URL is fragment-only + * + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + * @return bool + */ + private function _isFragmentOnly() + { + return ( + $this->_fragment !== false + && $this->_query === false + && $this->_path === '' + && $this->_port === false + && $this->_host === false + && $this->_userinfo === false + && $this->_scheme === false + ); + } + /** * Removes dots as described in RFC 3986, section 5.2.4, e.g. * "/foo/../bar/baz" => "/bar/baz" @@ -781,43 +975,52 @@ class Net_URL2 */ public static function removeDotSegments($path) { + $path = (string) $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) { - if (substr($path, 0, 2) == './') { + $loopLimit = 256; + $j = 0; + while ('' !== $path && $j++ < $loopLimit) { + if (substr($path, 0, 2) === './') { // Step 2.A $path = substr($path, 2); - } elseif (substr($path, 0, 3) == '../') { + } elseif (substr($path, 0, 3) === '../') { // Step 2.A $path = substr($path, 3); - } elseif (substr($path, 0, 3) == '/./' || $path == '/.') { + } elseif (substr($path, 0, 3) === '/./' || $path === '/.') { // Step 2.B $path = '/' . substr($path, 3); - } elseif (substr($path, 0, 4) == '/../' || $path == '/..') { + } elseif (substr($path, 0, 4) === '/../' || $path === '/..') { // Step 2.C $path = '/' . substr($path, 4); $i = strrpos($output, '/'); $output = $i === false ? '' : substr($output, 0, $i); - } elseif ($path == '.' || $path == '..') { + } elseif ($path === '.' || $path === '..') { // Step 2.D $path = ''; } else { // Step 2.E - $i = strpos($path, '/'); - if ($i === 0) { - $i = strpos($path, '/', 1); - } + $i = strpos($path, '/', $path[0] === '/'); if ($i === false) { - $i = strlen($path); + $output .= $path; + $path = ''; + break; } $output .= substr($path, 0, $i); $path = substr($path, $i); } } + if ($path !== '') { + $message = sprintf( + 'Unable to remove dot segments; hit loop limit %d (left: %s)', + $j, var_export($path, true) + ); + trigger_error($message, E_USER_WARNING); + } + return $output; } @@ -826,22 +1029,25 @@ class Net_URL2 * Similar to PHP's rawurlencode(), except that it also encodes ~ in PHP * 5.2.x and earlier. * - * @param $raw the string to encode + * @param string $string string to encode + * * @return string */ public static function urlencode($string) { - $encoded = rawurlencode($string); - // This is only necessary in PHP < 5.3. - $encoded = str_replace('%7E', '~', $encoded); - return $encoded; + $encoded = rawurlencode($string); + + // This is only necessary in PHP < 5.3. + $encoded = str_replace('%7E', '~', $encoded); + return $encoded; } /** * Returns a Net_URL2 instance representing the canonical URL of the * currently executing PHP script. - * - * @return string + * + * @throws Exception + * @return string */ public static function getCanonical() { @@ -854,10 +1060,10 @@ class Net_URL2 $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) { - + $port = $_SERVER['SERVER_PORT']; + if ($url->_scheme == 'http' && $port != 80 + || $url->_scheme == 'https' && $port != 443 + ) { $url->_port = $port; } return $url; @@ -877,7 +1083,8 @@ class Net_URL2 * Returns a Net_URL2 instance representing the URL used to retrieve the * current request. * - * @return Net_URL2 + * @throws Exception + * @return $this */ public static function getRequested() { @@ -895,34 +1102,118 @@ class Net_URL2 } /** - * Sets the specified option. + * Returns the value of the specified option. * - * @param string $optionName a self::OPTION_ constant - * @param mixed $value option value + * @param string $optionName The name of the option to retrieve * - * @return void - * @see self::OPTION_STRICT - * @see self::OPTION_USE_BRACKETS - * @see self::OPTION_ENCODE_KEYS + * @return mixed */ - function setOption($optionName, $value) + public function getOption($optionName) { - if (!array_key_exists($optionName, $this->_options)) { - return false; + return isset($this->_options[$optionName]) + ? $this->_options[$optionName] : false; + } + + /** + * A simple version of http_build_query in userland. The encoded string is + * percentage encoded according to RFC 3986. + * + * @param array $data An array, which has to be converted into + * QUERY_STRING. Anything is possible. + * @param string $separator Separator {@link self::OPTION_SEPARATOR_OUTPUT} + * @param string $key For stacked values (arrays in an array). + * + * @return string + */ + protected function buildQuery(array $data, $separator, $key = null) + { + $query = array(); + $drop_names = ( + $this->_options[self::OPTION_DROP_SEQUENCE] === true + && array_keys($data) === array_keys(array_values($data)) + ); + foreach ($data as $name => $value) { + if ($this->getOption(self::OPTION_ENCODE_KEYS) === true) { + $name = rawurlencode($name); + } + if ($key !== null) { + if ($this->getOption(self::OPTION_USE_BRACKETS) === true) { + $drop_names && $name = ''; + $name = $key . '[' . $name . ']'; + } else { + $name = $key; + } + } + if (is_array($value)) { + $query[] = $this->buildQuery($value, $separator, $name); + } else { + $query[] = $name . '=' . rawurlencode($value); + } } - $this->_options[$optionName] = $value; + return implode($separator, $query); } /** - * Returns the value of the specified option. + * This method uses a regex to parse the url into the designated parts. * - * @param string $optionName The name of the option to retrieve + * @param string $url URL * - * @return mixed + * @return void + * @uses self::$_scheme, self::setAuthority(), self::$_path, self::$_query, + * self::$_fragment + * @see __construct */ - function getOption($optionName) + protected function parseUrl($url) { - return isset($this->_options[$optionName]) - ? $this->_options[$optionName] : false; + // The regular expression is copied verbatim from RFC 3986, appendix B. + // The expression does not validate the URL but matches any string. + preg_match( + '(^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)', + $url, $matches + ); + + // "path" is always present (possibly as an empty string); the rest + // are optional. + $this->_scheme = !empty($matches[1]) ? $matches[2] : false; + $this->setAuthority(!empty($matches[3]) ? $matches[4] : false); + $this->_path = $this->_encodeData($matches[5]); + $this->_query = !empty($matches[6]) + ? $this->_encodeData($matches[7]) + : false + ; + $this->_fragment = !empty($matches[8]) ? $matches[9] : false; + } + + /** + * Encode characters that might have been forgotten to encode when passing + * in an URL. Applied onto Userinfo, Path and Query. + * + * @param string $url URL + * + * @return string + * @see parseUrl + * @see setAuthority + * @link https://pear.php.net/bugs/bug.php?id=20425 + */ + private function _encodeData($url) + { + return preg_replace_callback( + '([\x-\x20\x22\x3C\x3E\x7F-\xFF]+)', + array($this, '_encodeCallback'), $url + ); + } + + /** + * callback for encoding character data + * + * @param array $matches Matches + * + * @return string + * @see _encodeData + * @SuppressWarnings(PHPMD.UnusedPrivateMethod) + */ + private function _encodeCallback(array $matches) + { + return rawurlencode($matches[0]); } }