Skip to content

Commit 75c9fc1

Browse files
committed
Fix php-curl-class#625: Update Url class to handle IPv6 addresses with bracket notation.
1 parent 1f62ac4 commit 75c9fc1

File tree

2 files changed

+134
-3
lines changed

2 files changed

+134
-3
lines changed

src/Curl/Url.php

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,68 @@ private function absolutizeUrl()
156156
* Parse url into components of a URI as specified by RFC 3986.
157157
*/
158158
private function parseUrl($url)
159+
{
160+
// RFC 3986 - Parsing a URI Reference with a Regular Expression.
161+
// ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
162+
// 12 3 4 5 6 7 8 9
163+
//
164+
// "http://www.ics.uci.edu/pub/ietf/uri/#Related"
165+
// $1 = http: (scheme)
166+
// $2 = http (scheme)
167+
// $3 = //www.ics.uci.edu (ignore)
168+
// $4 = www.ics.uci.edu (authority)
169+
// $5 = /pub/ietf/uri/ (path)
170+
// $6 = <undefined> (ignore)
171+
// $7 = <undefined> (query)
172+
// $8 = #Related (ignore)
173+
// $9 = Related (fragment)
174+
preg_match('/^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/', $url, $output_array);
175+
176+
$parts = array();
177+
if (isset($output_array['1']) && $output_array['1'] !== '') {
178+
$parts['scheme'] = $output_array['1'];
179+
}
180+
if (isset($output_array['2']) && $output_array['2'] !== '') {
181+
$parts['scheme'] = $output_array['2'];
182+
}
183+
if (isset($output_array['4']) && $output_array['4'] !== '') {
184+
// authority = [ userinfo "@" ] host [ ":" port ]
185+
$parts['host'] = $output_array['4'];
186+
if (strpos($parts['host'], ':') !== false) {
187+
$host_parts = explode(':', $output_array['4']);
188+
$parts['port'] = array_pop($host_parts);
189+
$parts['host'] = implode(':', $host_parts);
190+
if (strpos($parts['host'], '@') !== false) {
191+
$host_parts = explode('@', $parts['host']);
192+
$parts['host'] = array_pop($host_parts);
193+
$parts['user'] = implode('@', $host_parts);
194+
if (strpos($parts['user'], ':') !== false) {
195+
$user_parts = explode(':', $parts['user'], 2);
196+
$parts['user'] = array_shift($user_parts);
197+
$parts['pass'] = implode(':', $user_parts);
198+
}
199+
}
200+
}
201+
}
202+
if (isset($output_array['5']) && $output_array['5'] !== '') {
203+
$parts['path'] = $this->percentEncodeChars($output_array['5']);
204+
}
205+
if (isset($output_array['7']) && $output_array['7'] !== '') {
206+
$parts['query'] = $output_array['7'];
207+
}
208+
if (isset($output_array['9']) && $output_array['9'] !== '') {
209+
$parts['fragment'] = $output_array['9'];
210+
}
211+
return $parts;
212+
}
213+
214+
/**
215+
* Percent-encode characters.
216+
*
217+
* Percent-encode characters to represent a data octet in a component when
218+
* that octet's corresponding character is outside the allowed set.
219+
*/
220+
private function percentEncodeChars($chars)
159221
{
160222
// ALPHA = A-Z / a-z
161223
$alpha = 'A-Za-z';
@@ -177,14 +239,14 @@ private function parseUrl($url)
177239
$hexdig .= 'a-f';
178240

179241
$pattern = '/(?:[^' . $unreserved . $sub_delims . preg_quote(':@%/?', '/') . ']++|%(?![' . $hexdig . ']{2}))/';
180-
$url = preg_replace_callback(
242+
$percent_encoded_chars = preg_replace_callback(
181243
$pattern,
182244
function ($matches) {
183245
return rawurlencode($matches[0]);
184246
},
185-
$url
247+
$chars
186248
);
187-
return parse_url($url);
249+
return $percent_encoded_chars;
188250
}
189251

190252
/**

tests/PHPCurlClass/UrlTest.php

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,73 @@ public function testCyrillicChars()
7676
$url = new Url($original_url);
7777
$this->assertEquals($expected_url, $url);
7878
}
79+
80+
public function testParseUrlSyntaxComponents()
81+
{
82+
// RFC 3986 - Syntax Components.
83+
// The following are two example URIs and their component parts:
84+
//
85+
// foo://example.com:8042/over/there?name=ferret#nose
86+
// \_/ \______________/\_________/ \_________/ \__/
87+
// | | | | |
88+
// scheme authority path query fragment
89+
$input_url = 'foo://example.com:8042/over/there?name=ferret#nose';
90+
$expected_parts = array(
91+
'scheme' => 'foo',
92+
'host' => 'example.com',
93+
'port' => '8042',
94+
'path' => '/over/there',
95+
'query' => 'name=ferret',
96+
'fragment' => 'nose',
97+
);
98+
99+
$this->assertEquals($expected_parts, parse_url($input_url));
100+
101+
$reflector = new \ReflectionClass('Curl\Url');
102+
$reflection_method = $reflector->getMethod('parseUrl');
103+
$reflection_method->setAccessible(true);
104+
105+
$url = new Url(null);
106+
$result = $reflection_method->invoke($url, $input_url);
107+
$this->assertEquals($expected_parts, $result);
108+
}
109+
110+
public function testParseUrlExample()
111+
{
112+
$input_url = 'http://username:password@hostname:9090/path?arg=value#anchor';
113+
$expected_parts = array(
114+
'scheme' => 'http',
115+
'host' => 'hostname',
116+
'port' => '9090',
117+
'user' => 'username',
118+
'pass' => 'password',
119+
'path' => '/path',
120+
'query' => 'arg=value',
121+
'fragment' => 'anchor',
122+
);
123+
124+
$this->assertEquals($expected_parts, parse_url($input_url));
125+
126+
$reflector = new \ReflectionClass('Curl\Url');
127+
$reflection_method = $reflector->getMethod('parseUrl');
128+
$reflection_method->setAccessible(true);
129+
130+
$url = new Url(null);
131+
$result = $reflection_method->invoke($url, $input_url);
132+
$this->assertEquals($expected_parts, $result);
133+
}
134+
135+
public function testIpv6NoPort()
136+
{
137+
$expected_url = 'http://[::1]/test';
138+
$actual_url = new Url($expected_url);
139+
$this->assertEquals($expected_url, $actual_url);
140+
}
141+
142+
public function testIpv6Port()
143+
{
144+
$expected_url = 'http://[::1]:80/test';
145+
$actual_url = new Url($expected_url);
146+
$this->assertEquals($expected_url, $actual_url);
147+
}
79148
}

0 commit comments

Comments
 (0)