1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* @file CurlAdapter.php |
5
|
|
|
* @brief This file contains the CurlAdapter class. |
6
|
|
|
* @details |
7
|
|
|
* @author Filippo F. Fadda |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
|
11
|
|
|
namespace Surfer\Adapter; |
12
|
|
|
|
13
|
|
|
|
14
|
|
|
use Surfer\Message\Request; |
15
|
|
|
use Surfer\Message\Response; |
16
|
|
|
use Surfer\Hook; |
17
|
|
|
|
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* @brief An HTTP 1.1 client adapter using cURL. |
21
|
|
|
* @nosubgrouping |
22
|
|
|
* @attention To use this client adapter, cURL must be installed on server. |
23
|
|
|
*/ |
24
|
|
|
class CurlAdapter extends AbstractAdapter { |
25
|
|
|
|
26
|
|
|
// cURL handle. |
27
|
|
|
private $handle; |
28
|
|
|
|
29
|
|
|
// Timeout is seconds. |
30
|
|
|
private $timeout = NULL; |
31
|
|
|
|
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @copydoc AbstractAdapter::__construct() |
35
|
|
|
*/ |
36
|
|
|
public function __construct($server = parent::DEFAULT_SERVER, $userName = "", $password = "") { |
37
|
|
|
$this->initialize(); |
38
|
|
|
|
39
|
|
|
parent::__construct($server, $userName, $password); |
40
|
|
|
|
41
|
|
|
// Init cURL. |
42
|
|
|
$this->handle = curl_init(); |
43
|
|
|
} |
44
|
|
|
|
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @brief Destroys the cURL handle. |
48
|
|
|
*/ |
49
|
|
|
public function __destruct() { |
50
|
|
|
curl_close($this->handle); |
51
|
|
|
} |
52
|
|
|
|
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @copydoc AbstractAdapter::initialize() |
56
|
|
|
*/ |
57
|
|
|
public function initialize() { |
58
|
|
|
if (!extension_loaded("curl")) |
59
|
|
|
throw new \RuntimeException("The cURL extension is not loaded."); |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* @copydoc AbstractAdapter::setTimeout() |
65
|
|
|
*/ |
66
|
|
|
public function setTimeout($seconds) { |
67
|
|
|
$this->timeout = (int)$seconds; |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @copydoc AbstractAdapter::send() |
73
|
|
|
* @bug https://github.com/dedalozzo/eoc-client/issues/2 |
74
|
|
|
*/ |
75
|
|
|
public function send(Request $request, Hook\IChunkHook $chunkHook = NULL) { |
76
|
|
|
$opts = []; |
77
|
|
|
|
78
|
|
|
// Resets all the cURL options. The curl_reset() function is available only since PHP 5.5. |
79
|
|
|
if (function_exists('curl_reset')) |
80
|
|
|
curl_reset($this->handle); |
81
|
|
|
|
82
|
|
|
// Sets the methods and its related options. |
83
|
|
|
switch ($request->getMethod()) { |
84
|
|
|
|
85
|
|
|
// GET method. |
86
|
|
|
case Request::GET_METHOD: |
87
|
|
|
$opts[CURLOPT_HTTPGET] = TRUE; |
88
|
|
|
break; |
89
|
|
|
|
90
|
|
|
// POST method. |
91
|
|
|
case Request::POST_METHOD: |
92
|
|
|
$opts[CURLOPT_POST] = TRUE; |
93
|
|
|
|
94
|
|
|
// The full data to post in a HTTP "POST" operation. To post a file, prepend a filename with @ and use the full |
95
|
|
|
// path. This can either be passed as a urlencoded string like 'para1=val1¶2=val2&...' or as an array with |
96
|
|
|
// the field name as key and field data as value. If value is an array, the Content-Type header will be set to |
97
|
|
|
// multipart/form-data. |
98
|
|
|
$opts[CURLOPT_POSTFIELDS] = $request->getBody(); |
99
|
|
|
break; |
100
|
|
|
|
101
|
|
|
// PUT method. |
102
|
|
|
case Request::PUT_METHOD: |
103
|
|
|
$opts[CURLOPT_PUT] = TRUE; |
104
|
|
|
|
105
|
|
|
// Often a request contains data in the form of a JSON object. Since cURL is just able to read data from a file, |
106
|
|
|
// but we can't create a temporary file because it's a too much expensive operation, the code below uses a faster |
107
|
|
|
// and efficient memory stream. |
108
|
|
|
if ($request->hasBody()) { |
109
|
|
|
if ($fd = fopen("php://memory", "r+")) { // Try to create a temporary file in memory. |
110
|
|
|
fputs($fd, $request->getBody()); // Writes the message body. |
111
|
|
|
rewind($fd); // Sets the pointer to the beginning of the file stream. |
112
|
|
|
|
113
|
|
|
$opts[CURLOPT_INFILE] = $fd; |
114
|
|
|
$opts[CURLOPT_INFILESIZE] = $request->getBodyLength(); |
115
|
|
|
} |
116
|
|
|
else |
117
|
|
|
throw new \RuntimeException("Cannot create the stream."); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
break; |
121
|
|
|
|
122
|
|
|
// DELETE method. |
123
|
|
|
case Request::DELETE_METHOD: |
124
|
|
|
$opts[CURLOPT_CUSTOMREQUEST] = Request::DELETE_METHOD; |
125
|
|
|
break; |
126
|
|
|
|
127
|
|
|
// COPY or any other custom method. |
128
|
|
|
default: |
129
|
|
|
$opts[CURLOPT_CUSTOMREQUEST] = $request->getMethod(); |
130
|
|
|
|
131
|
|
|
} // switch |
132
|
|
|
|
133
|
|
|
// Sets the request Uniform Resource Locator. |
134
|
|
|
$opts[CURLOPT_URL] = $this->scheme.$this->host.":".$this->port.$request->getPath().$request->getQueryString(); |
135
|
|
|
|
136
|
|
|
// Includes the header in the output. We need this because our Response object will parse them. |
137
|
|
|
// NOTE: we don't include header anymore, because we use the option CURLOPT_HEADERFUNCTION. |
138
|
|
|
//$opts[CURLOPT_HEADER] = TRUE; |
139
|
|
|
|
140
|
|
|
// Returns the transfer as a string of the return value of curl_exec() instead of outputting it out directly. |
141
|
|
|
$opts[CURLOPT_RETURNTRANSFER] = TRUE; |
142
|
|
|
|
143
|
|
|
// Sets the protocol version to be used. cURL constants have different values. |
144
|
|
|
$opts[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1; |
145
|
|
|
|
146
|
|
|
// Sets basic authentication. |
147
|
|
|
if (!empty($this->userName)) { |
148
|
|
|
$opts[CURLOPT_HTTPAUTH] = CURLAUTH_BASIC; |
149
|
|
|
$opts[CURLOPT_USERPWD] = $this->userName.":".$this->password; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
// Sets the previous options. |
153
|
|
|
curl_setopt_array($this->handle, $opts); |
154
|
|
|
|
155
|
|
|
// This fix a known cURL bug: see http://the-stickman.com/web-development/php-and-curl-disabling-100-continue-header/ |
156
|
|
|
// cURL sets the Expect header field automatically, ignoring the fact that a client may not need it for the specific |
157
|
|
|
// request. |
158
|
|
|
if (!$request->hasHeaderField(Request::EXPECT_HF)) |
159
|
|
|
curl_setopt($this->handle, CURLOPT_HTTPHEADER, array("Expect:")); |
160
|
|
|
|
161
|
|
|
// Sets the request header. |
162
|
|
|
// Due to a stupid bug, using curl_setopt_array(), cURL doesn't override the Content-Type header field. So we must |
163
|
|
|
// set the header using, instead, curl_stopt() |
164
|
|
|
// $opts[CURLOPT_HTTPHEADER] = $request->getHeaderAsArray(); |
165
|
|
|
curl_setopt($this->handle, CURLOPT_HTTPHEADER, $request->getHeaderAsArray()); |
166
|
|
|
|
167
|
|
|
// Here we use this option because we might have a response without body. This may happen because we are supporting |
168
|
|
|
// chunk responses, and sometimes we want trigger an hook function to let the user perform operations on coming |
169
|
|
|
// chunks. |
170
|
|
|
$header = ""; |
171
|
|
|
curl_setopt($this->handle, CURLOPT_HEADERFUNCTION, |
172
|
|
|
function($unused, $buffer) use (&$header) { |
173
|
|
|
$header .= $buffer; |
174
|
|
|
return strlen($buffer); |
175
|
|
|
}); |
176
|
|
|
|
177
|
|
|
// When the hook function is provided, we set the CURLOPT_WRITEFUNCTION so cURL will call the hook function for each |
178
|
|
|
// response chunk read. |
179
|
|
|
if (isset($chunkHook)) { |
180
|
|
|
curl_setopt($this->handle, CURLOPT_WRITEFUNCTION, |
181
|
|
|
function($unused, $buffer) use ($chunkHook) { |
182
|
|
|
$chunkHook->process($buffer); |
183
|
|
|
return strlen($buffer); |
184
|
|
|
}); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
if ($this->timeout) |
188
|
|
|
curl_setopt($this->handle, CURLOPT_TIMEOUT, $this->timeout); |
189
|
|
|
|
190
|
|
|
if ($result = curl_exec($this->handle)) { |
191
|
|
|
$response = new Response($header); |
192
|
|
|
$response->setBody($result); |
193
|
|
|
return $response; |
194
|
|
|
} |
195
|
|
|
else { |
196
|
|
|
$error = curl_error($this->handle); |
197
|
|
|
throw new \RuntimeException($error); |
198
|
|
|
} |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
} |