Skip to content

Commit ce9ff3c

Browse files
committed
Fix php-curl-class#347: Add basic support for requests using relative paths
1 parent dd8493d commit ce9ff3c

File tree

7 files changed

+447
-37
lines changed

7 files changed

+447
-37
lines changed

examples/get_relative.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
require __DIR__ . '/../vendor/autoload.php';
3+
4+
use \Curl\Curl;
5+
6+
$curl = new Curl('https://www.example.com/api/');
7+
8+
// https://www.example.com/api/test?key=value
9+
$response = $curl->get('test', array(
10+
'key' => 'value',
11+
));
12+
assert('https://www.example.com/api/test?key=value' === $curl->url);
13+
assert($curl->url === $curl->effectiveUrl);
14+
15+
// https://www.example.com/root?key=value
16+
$response = $curl->get('/root', array(
17+
'key' => 'value',
18+
));
19+
assert('https://www.example.com/root?key=value' === $curl->url);
20+
assert($curl->url === $curl->effectiveUrl);

src/Curl/Curl.php

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ class Curl
2525
public $httpStatusCode = 0;
2626
public $httpErrorMessage = null;
2727

28-
public $baseUrl = null;
2928
public $url = null;
3029
public $requestHeaders = null;
3130
public $responseHeaders = null;
@@ -59,7 +58,7 @@ class Curl
5958
private $defaultDecoder = null;
6059

6160
public static $RFC2616 = array(
62-
// RFC2616: "any CHAR except CTLs or separators".
61+
// RFC 2616: "any CHAR except CTLs or separators".
6362
// CHAR = <any US-ASCII character (octets 0 - 127)>
6463
// CTL = <any US-ASCII control character
6564
// (octets 0 - 31) and DEL (127)>
@@ -76,7 +75,7 @@ class Curl
7675
'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '|', '~',
7776
);
7877
public static $RFC6265 = array(
79-
// RFC6265: "US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash".
78+
// RFC 6265: "US-ASCII characters excluding CTLs, whitespace DQUOTE, comma, semicolon, and backslash".
8079
// %x21
8180
'!',
8281
// %x23-2B
@@ -261,7 +260,7 @@ public function delete($url, $query_parameters = array(), $data = array())
261260
if (is_array($url)) {
262261
$data = $query_parameters;
263262
$query_parameters = $url;
264-
$url = $this->baseUrl;
263+
$url = (string)$this->url;
265264
}
266265

267266
$this->setUrl($url, $query_parameters);
@@ -390,6 +389,10 @@ public function exec($ch = null)
390389
}
391390
$this->errorMessage = $this->curlError ? $this->curlErrorMessage : $this->httpErrorMessage;
392391

392+
// Reset select deferred properties so that they may be recalculated.
393+
unset($this->effectiveUrl);
394+
unset($this->totalTime);
395+
393396
// Allow multicurl to attempt retry as needed.
394397
if ($this->isChildOfMultiCurl) {
395398
return;
@@ -433,7 +436,7 @@ public function get($url, $data = array())
433436
{
434437
if (is_array($url)) {
435438
$data = $url;
436-
$url = $this->baseUrl;
439+
$url = (string)$this->url;
437440
}
438441
$this->setUrl($url, $data);
439442
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'GET');
@@ -487,7 +490,7 @@ public function head($url, $data = array())
487490
{
488491
if (is_array($url)) {
489492
$data = $url;
490-
$url = $this->baseUrl;
493+
$url = (string)$this->url;
491494
}
492495
$this->setUrl($url, $data);
493496
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'HEAD');
@@ -508,7 +511,7 @@ public function options($url, $data = array())
508511
{
509512
if (is_array($url)) {
510513
$data = $url;
511-
$url = $this->baseUrl;
514+
$url = (string)$this->url;
512515
}
513516
$this->setUrl($url, $data);
514517
$this->removeHeader('Content-Length');
@@ -529,7 +532,7 @@ public function patch($url, $data = array())
529532
{
530533
if (is_array($url)) {
531534
$data = $url;
532-
$url = $this->baseUrl;
535+
$url = (string)$this->url;
533536
}
534537

535538
if (is_array($data) && empty($data)) {
@@ -572,7 +575,7 @@ public function post($url, $data = array(), $follow_303_with_post = false)
572575
if (is_array($url)) {
573576
$follow_303_with_post = (bool)$data;
574577
$data = $url;
575-
$url = $this->baseUrl;
578+
$url = (string)$this->url;
576579
}
577580

578581
$this->setUrl($url);
@@ -613,7 +616,7 @@ public function put($url, $data = array())
613616
{
614617
if (is_array($url)) {
615618
$data = $url;
616-
$url = $this->baseUrl;
619+
$url = (string)$this->url;
617620
}
618621
$this->setUrl($url);
619622
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'PUT');
@@ -642,7 +645,7 @@ public function search($url, $data = array())
642645
{
643646
if (is_array($url)) {
644647
$data = $url;
645-
$url = $this->baseUrl;
648+
$url = (string)$this->url;
646649
}
647650
$this->setUrl($url);
648651
$this->setOpt(CURLOPT_CUSTOMREQUEST, 'SEARCH');
@@ -1071,8 +1074,14 @@ public function setTimeout($seconds)
10711074
*/
10721075
public function setUrl($url, $mixed_data = '')
10731076
{
1074-
$this->baseUrl = $url;
1075-
$this->url = $this->buildUrl($url, $mixed_data);
1077+
$built_url = $this->buildUrl($url, $mixed_data);
1078+
1079+
if ($this->url === null) {
1080+
$this->url = (string)new Url($built_url);
1081+
} else {
1082+
$this->url = (string)new Url($this->url, $built_url);
1083+
}
1084+
10761085
$this->setOpt(CURLOPT_URL, $this->url);
10771086
}
10781087

src/Curl/StrUtil.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Curl;
4+
5+
class StrUtil
6+
{
7+
/**
8+
* Return true when $haystack starts with $needle.
9+
*
10+
* @access public
11+
* @param $haystack
12+
* @param $needle
13+
*
14+
* @return bool
15+
*/
16+
public static function startsWith($haystack, $needle)
17+
{
18+
return mb_substr($haystack, 0, mb_strlen($needle)) === $needle;
19+
}
20+
}

src/Curl/Url.php

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
<?php
2+
3+
namespace Curl;
4+
5+
use Curl\StrUtil;
6+
7+
class Url
8+
{
9+
private $baseUrl = null;
10+
private $relativeUrl = null;
11+
12+
public function __construct($base_url, $relative_url = null)
13+
{
14+
$this->baseUrl = $base_url;
15+
$this->relativeUrl = $relative_url;
16+
}
17+
18+
public function __toString()
19+
{
20+
return $this->absolutizeUrl();
21+
}
22+
23+
/**
24+
* Remove dot segments.
25+
*
26+
* Interpret and remove the special "." and ".." path segments from a referenced path.
27+
*/
28+
public static function removeDotSegments($input)
29+
{
30+
// 1. The input buffer is initialized with the now-appended path
31+
// components and the output buffer is initialized to the empty
32+
// string.
33+
$output = '';
34+
35+
// 2. While the input buffer is not empty, loop as follows:
36+
while (!empty($input)) {
37+
38+
// A. If the input buffer begins with a prefix of "../" or "./",
39+
// then remove that prefix from the input buffer; otherwise,
40+
if (StrUtil::startsWith($input, '../')) {
41+
$input = substr($input, 3);
42+
} elseif (StrUtil::startsWith($input, './')) {
43+
$input = substr($input, 2);
44+
45+
// B. if the input buffer begins with a prefix of "/./" or "/.",
46+
// where "." is a complete path segment, then replace that
47+
// prefix with "/" in the input buffer; otherwise,
48+
} elseif (StrUtil::startsWith($input, '/./')) {
49+
$input = substr($input, 2);
50+
} elseif ($input === '/.') {
51+
$input = '/';
52+
53+
// C. if the input buffer begins with a prefix of "/../" or "/..",
54+
// where ".." is a complete path segment, then replace that
55+
// prefix with "/" in the input buffer and remove the last
56+
// segment and its preceding "/" (if any) from the output
57+
// buffer; otherwise,
58+
} elseif (StrUtil::startsWith($input, '/../')) {
59+
$input = substr($input, 3);
60+
$output = substr_replace($output, '', mb_strrpos($output, '/'));
61+
} elseif ($input === '/..') {
62+
$input = '/';
63+
$output = substr_replace($output, '', mb_strrpos($output, '/'));
64+
65+
// D. if the input buffer consists only of "." or "..", then remove
66+
// that from the input buffer; otherwise,
67+
} elseif ($input === '.' || $input === '..') {
68+
$input = '';
69+
70+
// E. move the first path segment in the input buffer to the end of
71+
// the output buffer, including the initial "/" character (if
72+
// any) and any subsequent characters up to, but not including,
73+
// the next "/" character or the end of the input buffer.
74+
} elseif (!(($pos = mb_strpos($input, '/', 1)) === false)) {
75+
$output .= substr($input, 0, $pos);
76+
$input = substr_replace($input, '', 0, $pos);
77+
} else {
78+
$output .= $input;
79+
$input = '';
80+
}
81+
}
82+
83+
// 3. Finally, the output buffer is returned as the result of
84+
// remove_dot_segments.
85+
return $output . $input;
86+
}
87+
88+
/**
89+
* Absolutize url.
90+
*
91+
* Combine the base and relative url into an absolute url.
92+
*/
93+
private function absolutizeUrl()
94+
{
95+
$b = $this->parseUrl($this->baseUrl);
96+
97+
if (!($this->relativeUrl === null)) {
98+
$r = $this->parseUrl($this->relativeUrl);
99+
100+
// Copy relative parts to base url.
101+
if (isset($r['scheme'])) {
102+
$b['scheme'] = $r['scheme'];
103+
}
104+
if (isset($r['host'])) {
105+
$b['host'] = $r['host'];
106+
}
107+
if (isset($r['port'])) {
108+
$b['port'] = $r['port'];
109+
}
110+
if (isset($r['user'])) {
111+
$b['user'] = $r['user'];
112+
}
113+
if (isset($r['pass'])) {
114+
$b['pass'] = $r['pass'];
115+
}
116+
117+
if (!isset($r['path']) || $r['path'] === '') {
118+
$r['path'] = '/';
119+
}
120+
// Merge relative url with base when relative url's path doesn't start with a slash.
121+
if (!(StrUtil::startsWith($r['path'], '/'))) {
122+
$base = mb_strrchr($b['path'], '/', true);
123+
if ($base === false) {
124+
$base = '';
125+
}
126+
$r['path'] = $base . '/' . $r['path'];
127+
}
128+
$b['path'] = $r['path'];
129+
$b['path'] = $this->removeDotSegments($b['path']);
130+
131+
if (isset($r['query'])) {
132+
$b['query'] = $r['query'];
133+
}
134+
if (isset($r['fragment'])) {
135+
$b['fragment'] = $r['fragment'];
136+
}
137+
}
138+
139+
if (!isset($b['path'])) {
140+
$b['path'] = '/';
141+
}
142+
143+
$absolutized_url = $this->unparseUrl($b);
144+
return $absolutized_url;
145+
}
146+
147+
/**
148+
* Parse url.
149+
*
150+
* Parse url into components of a URI as specified by RFC 3986.
151+
*/
152+
private function parseUrl($url)
153+
{
154+
return parse_url($url);
155+
}
156+
157+
/**
158+
* Unparse url.
159+
*
160+
* Combine url components into a url.
161+
*/
162+
private function unparseUrl($parsed_url) {
163+
$scheme = isset($parsed_url['scheme']) ? $parsed_url['scheme'] . '://' : '';
164+
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
165+
$port = isset($parsed_url['port']) ? ':' . $parsed_url['port'] : '';
166+
$user = isset($parsed_url['user']) ? $parsed_url['user'] : '';
167+
$pass = isset($parsed_url['pass']) ? ':' . $parsed_url['pass'] : '';
168+
$pass = ($user || $pass) ? $pass . '@' : '';
169+
$path = isset($parsed_url['path']) ? $parsed_url['path'] : '';
170+
$query = isset($parsed_url['query']) ? '?' . $parsed_url['query'] : '';
171+
$fragment = isset($parsed_url['fragment']) ? '#' . $parsed_url['fragment'] : '';
172+
$unparsed_url = $scheme . $user . $pass . $host . $port . $path . $query . $fragment;
173+
return $unparsed_url;
174+
}
175+
}

0 commit comments

Comments
 (0)