1 | <?php |
||
2 | /** |
||
3 | * File containing the {@link RequestHelper} class. |
||
4 | * @package Application Utils |
||
5 | * @subpackage RequestHelper |
||
6 | * @see RequestHelper |
||
7 | */ |
||
8 | |||
9 | namespace AppUtils; |
||
10 | |||
11 | use CurlHandle; |
||
12 | |||
13 | /** |
||
14 | * Handles sending POST requests with file attachments and regular variables. |
||
15 | * Creates the raw request headers required for the request and sends them |
||
16 | * using file_get_contents with the according context parameters. |
||
17 | * |
||
18 | * @package Application Utils |
||
19 | * @subpackage RequestHelper |
||
20 | * @author Sebastian Mordziol <[email protected]> |
||
21 | */ |
||
22 | class RequestHelper |
||
23 | { |
||
24 | public const FILETYPE_TEXT = 'text/plain'; |
||
25 | public const FILETYPE_XML = 'text/xml'; |
||
26 | public const FILETYPE_HTML = 'text/html'; |
||
27 | |||
28 | public const ENCODING_UTF8 = 'UTF-8'; |
||
29 | |||
30 | public const TRANSFER_ENCODING_BASE64 = 'BASE64'; |
||
31 | public const TRANSFER_ENCODING_8BIT = '8BIT'; |
||
32 | public const TRANSFER_ENCODING_BINARY = 'BINARY'; |
||
33 | |||
34 | public const ERROR_REQUEST_FAILED = 17902; |
||
35 | public const ERROR_CURL_INIT_FAILED = 17903; |
||
36 | public const ERROR_CANNOT_OPEN_LOGFILE = 17904; |
||
37 | |||
38 | protected string $eol = "\r\n"; |
||
39 | protected string $mimeBoundary; |
||
40 | protected string $destination; |
||
41 | protected bool $verifySSL = true; |
||
42 | protected RequestHelper_Boundaries $boundaries; |
||
43 | protected ?RequestHelper_Response $response = null; |
||
44 | protected int $timeout = 30; // seconds |
||
45 | protected string $logfile = ''; |
||
46 | protected int $contentLength = 0; |
||
47 | |||
48 | /** |
||
49 | * @var array<string,string> |
||
50 | */ |
||
51 | protected $headers = array(); |
||
52 | |||
53 | /** |
||
54 | * @var resource|NULL |
||
55 | */ |
||
56 | protected $logfilePointer; |
||
57 | |||
58 | /** |
||
59 | * Creates a new request helper to send POST data to the specified destination URL. |
||
60 | * @param string $destinationURL |
||
61 | */ |
||
62 | public function __construct(string $destinationURL) |
||
63 | { |
||
64 | $this->destination = $destinationURL; |
||
65 | $this->mimeBoundary = str_repeat('-', 20).md5('request-helper-boundary'); |
||
66 | $this->boundaries = new RequestHelper_Boundaries($this); |
||
67 | } |
||
68 | |||
69 | public function getMimeBoundary() : string |
||
70 | { |
||
71 | return $this->mimeBoundary; |
||
72 | } |
||
73 | |||
74 | public function getMimeBody() : string |
||
75 | { |
||
76 | return $this->boundaries->render(); |
||
77 | } |
||
78 | |||
79 | public function getEOL() : string |
||
80 | { |
||
81 | return $this->eol; |
||
82 | } |
||
83 | |||
84 | /** |
||
85 | * Sets the timeout for the request, in seconds. If the request |
||
86 | * takes longer, it will be cancelled and an exception triggered. |
||
87 | * |
||
88 | * @param int $seconds |
||
89 | * @return RequestHelper |
||
90 | */ |
||
91 | public function setTimeout(int $seconds) : RequestHelper |
||
92 | { |
||
93 | $this->timeout = $seconds; |
||
94 | |||
95 | return $this; |
||
96 | } |
||
97 | |||
98 | public function getTimeout() : int |
||
99 | { |
||
100 | return $this->timeout; |
||
101 | } |
||
102 | |||
103 | /** |
||
104 | * Enables verbose logging of the CURL request, which |
||
105 | * is then redirected to the target file. |
||
106 | * |
||
107 | * @param string $targetFile |
||
108 | * @return RequestHelper |
||
109 | */ |
||
110 | public function enableLogging(string $targetFile) : RequestHelper |
||
111 | { |
||
112 | $this->logfile = $targetFile; |
||
113 | |||
114 | return $this; |
||
115 | } |
||
116 | |||
117 | /** |
||
118 | * Adds a file to be sent with the request. |
||
119 | * |
||
120 | * @param string $varName The variable name to send the file in |
||
121 | * @param string $fileName The name of the file as it should be received at the destination |
||
122 | * @param string $content The raw content of the file |
||
123 | * @param string $contentType The content type, use the constants to specify this |
||
124 | * @param string $encoding The encoding of the file, use the constants to specify this |
||
125 | */ |
||
126 | public function addFile(string $varName, string $fileName, string $content, string $contentType = '', string $encoding = '') : RequestHelper |
||
127 | { |
||
128 | $this->boundaries->addFile($varName, $fileName, $content, $contentType, $encoding); |
||
129 | |||
130 | return $this; |
||
131 | } |
||
132 | |||
133 | /** |
||
134 | * Adds arbitrary content. |
||
135 | * |
||
136 | * @param string $varName The variable name to send the content in. |
||
137 | * @param string $content |
||
138 | * @param string $contentType |
||
139 | * @return RequestHelper |
||
140 | */ |
||
141 | public function addContent(string $varName, string $content, string $contentType) : RequestHelper |
||
142 | { |
||
143 | $this->boundaries->addContent($varName, $content, $contentType); |
||
144 | |||
145 | return $this; |
||
146 | } |
||
147 | |||
148 | /** |
||
149 | * Adds a variable to be sent with the request. If it |
||
150 | * already exists, its value is overwritten. |
||
151 | * |
||
152 | * @param string $name |
||
153 | * @param string $value |
||
154 | * @return RequestHelper |
||
155 | */ |
||
156 | public function addVariable(string $name, string $value) : RequestHelper |
||
157 | { |
||
158 | $this->boundaries->addVariable($name, $value); |
||
159 | |||
160 | return $this; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Sets an HTTP header to include in the request. |
||
165 | * |
||
166 | * @param string $name |
||
167 | * @param string $value |
||
168 | * @return RequestHelper |
||
169 | */ |
||
170 | public function setHeader(string $name, string $value) : RequestHelper |
||
171 | { |
||
172 | $this->headers[$name] = $value; |
||
173 | |||
174 | return $this; |
||
175 | } |
||
176 | |||
177 | /** |
||
178 | * Disables SSL certificate checking. |
||
179 | * |
||
180 | * @return RequestHelper |
||
181 | */ |
||
182 | public function disableSSLChecks() : RequestHelper |
||
183 | { |
||
184 | $this->verifySSL = false; |
||
185 | return $this; |
||
186 | } |
||
187 | |||
188 | /** |
||
189 | * Sends the POST request to the destination, and returns |
||
190 | * the response text. |
||
191 | * |
||
192 | * The response object is stored internally, so after calling |
||
193 | * this method it may be retrieved at any moment using the |
||
194 | * {@link getResponse()} method. |
||
195 | * |
||
196 | * @return string |
||
197 | * @see RequestHelper::getResponse() |
||
198 | * @throws RequestHelper_Exception |
||
199 | * |
||
200 | * @see RequestHelper::ERROR_REQUEST_FAILED |
||
201 | */ |
||
202 | public function send() : string |
||
203 | { |
||
204 | $info = parseURL($this->destination); |
||
205 | |||
206 | $ch = $this->configureCURL($info); |
||
207 | |||
208 | $output = curl_exec($ch); |
||
209 | |||
210 | if(isset($this->logfilePointer)) |
||
211 | { |
||
212 | fclose($this->logfilePointer); |
||
213 | } |
||
214 | |||
215 | $info = curl_getinfo($ch); |
||
216 | |||
217 | $this->response = new RequestHelper_Response($this, $info); |
||
218 | |||
219 | // CURL will complain about an empty response when the |
||
220 | // server sends a 100-continue code. That should not be |
||
221 | // regarded as an error. |
||
222 | if($output === false && $this->response->getCode() !== 100) |
||
223 | { |
||
224 | $curlCode = curl_errno($ch); |
||
225 | |||
226 | $this->response->setError( |
||
227 | $curlCode, |
||
228 | curl_error($ch).' | Explanation: '.curl_strerror($curlCode) |
||
229 | ); |
||
230 | } |
||
231 | else |
||
232 | { |
||
233 | $this->response->setBody((string)$output); |
||
234 | } |
||
235 | |||
236 | curl_close($ch); |
||
237 | |||
238 | return $this->response->getResponseBody(); |
||
239 | } |
||
240 | |||
241 | /** |
||
242 | * Retrieves the request's body content. This is an alias |
||
243 | * for {@see RequestHelper::getMimeBody()}. |
||
244 | * |
||
245 | * @return string |
||
246 | * @see RequestHelper::getMimeBody() |
||
247 | */ |
||
248 | public function getBody() : string |
||
249 | { |
||
250 | return $this->getMimeBody(); |
||
251 | } |
||
252 | |||
253 | /** |
||
254 | * Creates a new CURL resource configured according to the |
||
255 | * request's settings. |
||
256 | * |
||
257 | * @return resource|CurlHandle |
||
258 | * @throws RequestHelper_Exception |
||
259 | */ |
||
260 | public static function createCURL() |
||
261 | { |
||
262 | $ch = curl_init(); |
||
263 | |||
264 | if($ch instanceof CurlHandle || is_resource($ch)) |
||
265 | { |
||
266 | return $ch; |
||
267 | } |
||
268 | |||
269 | throw new RequestHelper_Exception( |
||
270 | 'Could not initialize a new cURL instance.', |
||
271 | sprintf( |
||
272 | 'Calling curl_init failed to return a valid resource or instance. Given: [%s].', |
||
273 | parseVariable($ch)->enableType()->toString() |
||
274 | ), |
||
275 | self::ERROR_CURL_INIT_FAILED |
||
276 | ); |
||
277 | } |
||
278 | |||
279 | /** |
||
280 | * Creates a new CURL resource configured according to the |
||
281 | * request's settings. |
||
282 | * |
||
283 | * @param URLInfo $url |
||
284 | * @throws RequestHelper_Exception |
||
285 | * @return resource |
||
286 | */ |
||
287 | protected function configureCURL(URLInfo $url) |
||
288 | { |
||
289 | $ch = self::createCURL(); |
||
290 | |||
291 | $this->setHeader('Content-Length', (string)$this->boundaries->getContentLength()); |
||
292 | $this->setHeader('Content-Type', 'multipart/form-data; boundary=' . $this->mimeBoundary); |
||
293 | |||
294 | curl_setopt($ch, CURLOPT_POST, true); |
||
295 | curl_setopt($ch, CURLOPT_URL, $url->getNormalizedWithoutAuth()); |
||
296 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); |
||
297 | curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); |
||
298 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); |
||
299 | curl_setopt($ch, CURLOPT_HTTPHEADER, $this->renderHeaders()); |
||
300 | curl_setopt($ch, CURLOPT_POSTFIELDS, $this->boundaries->render()); |
||
301 | curl_setopt($ch, CURLOPT_FAILONERROR, true); |
||
302 | |||
303 | $loggingEnabled = $this->configureLogging($ch); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
304 | |||
305 | if(!$loggingEnabled) |
||
306 | { |
||
307 | curl_setopt($ch, CURLINFO_HEADER_OUT, true); |
||
308 | } |
||
309 | |||
310 | if($this->verifySSL) |
||
311 | { |
||
312 | curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); |
||
313 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); |
||
314 | } |
||
315 | |||
316 | if($url->hasUsername()) |
||
317 | { |
||
318 | curl_setopt($ch, CURLOPT_USERNAME, $url->getUsername()); |
||
319 | curl_setopt($ch, CURLOPT_PASSWORD, $url->getPassword()); |
||
320 | } |
||
321 | |||
322 | return $ch; |
||
0 ignored issues
–
show
|
|||
323 | } |
||
324 | |||
325 | /** |
||
326 | * @param resource $ch |
||
327 | * @return bool Whether logging is enabled. |
||
328 | * @throws RequestHelper_Exception |
||
329 | */ |
||
330 | protected function configureLogging($ch) : bool |
||
331 | { |
||
332 | if(empty($this->logfile)) |
||
333 | { |
||
334 | return false; |
||
335 | } |
||
336 | |||
337 | $res = fopen($this->logfile, 'wb+'); |
||
338 | |||
339 | if($res === false) |
||
340 | { |
||
341 | throw new RequestHelper_Exception( |
||
342 | 'Cannot open logfile for writing.', |
||
343 | sprintf('Tried accessing the file at [%s].', $this->logfile), |
||
344 | self::ERROR_CANNOT_OPEN_LOGFILE |
||
345 | ); |
||
346 | } |
||
347 | |||
348 | $this->logfilePointer = $res; |
||
349 | |||
350 | curl_setopt($ch, CURLOPT_VERBOSE, true); |
||
351 | curl_setopt($ch, CURLOPT_STDERR, $res); |
||
352 | |||
353 | return true; |
||
354 | } |
||
355 | |||
356 | /** |
||
357 | * Compiles the associative headers array into |
||
358 | * the format understood by CURL, namely an indexed |
||
359 | * array with one header string per entry. |
||
360 | * |
||
361 | * @return string[] |
||
362 | */ |
||
363 | protected function renderHeaders() : array |
||
364 | { |
||
365 | $result = array(); |
||
366 | |||
367 | $this->setHeader('Expect', ''); |
||
368 | |||
369 | foreach($this->headers as $name => $value) { |
||
370 | $result[] = $name.': '.$value; |
||
371 | } |
||
372 | |||
373 | return $result; |
||
374 | } |
||
375 | |||
376 | /** |
||
377 | * Retrieves the raw response header, in the form of an indexed |
||
378 | * array containing all response header lines. |
||
379 | * |
||
380 | * @return string[] |
||
381 | */ |
||
382 | public function getResponseHeader() : array |
||
383 | { |
||
384 | $response = $this->getResponse(); |
||
385 | |||
386 | if($response !== null) { |
||
387 | return $response->getHeaders(); |
||
388 | } |
||
389 | |||
390 | return array(); |
||
391 | } |
||
392 | |||
393 | /** |
||
394 | * After calling the {@link send()} method, this may be used to |
||
395 | * retrieve the response text from the POST request. |
||
396 | * |
||
397 | * @return RequestHelper_Response|NULL |
||
398 | */ |
||
399 | public function getResponse() : ?RequestHelper_Response |
||
400 | { |
||
401 | return $this->response; |
||
402 | } |
||
403 | |||
404 | /** |
||
405 | * Retrieves all headers set until now. |
||
406 | * |
||
407 | * @return array<string,string> |
||
408 | */ |
||
409 | public function getHeaders() : array |
||
410 | { |
||
411 | return $this->headers; |
||
412 | } |
||
413 | |||
414 | /** |
||
415 | * Retrieves the value of a header by its name. |
||
416 | * |
||
417 | * @param string $name |
||
418 | * @return string The header value, or an empty string if not set. |
||
419 | */ |
||
420 | public function getHeader(string $name) : string |
||
421 | { |
||
422 | return $this->headers[$name] ?? ''; |
||
423 | } |
||
424 | } |
||
425 |