Skip to content

Commit 7fdccc7

Browse files
authored
Merge pull request php-curl-class#474 from zachborboa/master
Add basic support for requests using relative paths
2 parents dd8493d + 7bce2d9 commit 7fdccc7

File tree

7 files changed

+446
-37
lines changed

7 files changed

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

0 commit comments

Comments
 (0)