1: | <?php |
2: | namespace Worldline\Connect\Sdk\Communication; |
3: | |
4: | use ErrorException; |
5: | use Exception; |
6: | use Robtimus\Multipart\MultipartFormData; |
7: | use UnexpectedValueException; |
8: | use Worldline\Connect\Sdk\CommunicatorConfiguration; |
9: | use Worldline\Connect\Sdk\Logging\BodyObfuscator; |
10: | use Worldline\Connect\Sdk\Logging\CommunicatorLogger; |
11: | use Worldline\Connect\Sdk\Logging\HeaderObfuscator; |
12: | use Worldline\Connect\Sdk\ProxyConfiguration; |
13: | |
14: | |
15: | |
16: | |
17: | |
18: | |
19: | class DefaultConnection implements Connection |
20: | { |
21: | |
22: | protected $multiHandle = null; |
23: | |
24: | |
25: | protected $communicatorLogger = null; |
26: | |
27: | |
28: | private $communicatorLoggerHelper = null; |
29: | |
30: | |
31: | private $connectTimeout = -1; |
32: | |
33: | |
34: | private $readTimeout = -1; |
35: | |
36: | |
37: | private $proxyConfiguration = null; |
38: | |
39: | |
40: | |
41: | |
42: | public function __construct(CommunicatorConfiguration $communicatorConfiguration = null) |
43: | { |
44: | if ($communicatorConfiguration) { |
45: | $this->connectTimeout = $communicatorConfiguration->getConnectTimeout(); |
46: | $this->readTimeout = $communicatorConfiguration->getReadTimeout(); |
47: | $this->proxyConfiguration = $communicatorConfiguration->getProxyConfiguration(); |
48: | } |
49: | } |
50: | |
51: | |
52: | |
53: | |
54: | public function __destruct() |
55: | { |
56: | if (!is_null($this->multiHandle)) { |
57: | curl_multi_close($this->multiHandle); |
58: | $this->multiHandle = null; |
59: | } |
60: | } |
61: | |
62: | |
63: | |
64: | |
65: | |
66: | |
67: | public function get($requestUri, $requestHeaders, callable $responseHandler) |
68: | { |
69: | $requestId = UuidGenerator::generatedUuid(); |
70: | $this->logRequest($requestId, 'GET', $requestUri, $requestHeaders); |
71: | try { |
72: | $response = $this->executeRequest('GET', $requestUri, $requestHeaders, '', $responseHandler); |
73: | if ($response) { |
74: | $this->logResponse($requestId, $requestUri, $response); |
75: | } |
76: | } catch (Exception $exception) { |
77: | $this->logException($requestId, $requestUri, $exception); |
78: | throw $exception; |
79: | } |
80: | } |
81: | |
82: | |
83: | |
84: | |
85: | |
86: | |
87: | public function delete($requestUri, $requestHeaders, callable $responseHandler) |
88: | { |
89: | $requestId = UuidGenerator::generatedUuid(); |
90: | $this->logRequest($requestId, 'DELETE', $requestUri, $requestHeaders); |
91: | try { |
92: | $response = $this->executeRequest('DELETE', $requestUri, $requestHeaders, '', $responseHandler); |
93: | if ($response) { |
94: | $this->logResponse($requestId, $requestUri, $response); |
95: | } |
96: | } catch (Exception $exception) { |
97: | $this->logException($requestId, $requestUri, $exception); |
98: | throw $exception; |
99: | } |
100: | } |
101: | |
102: | |
103: | |
104: | |
105: | |
106: | |
107: | |
108: | public function post($requestUri, $requestHeaders, $body, callable $responseHandler) |
109: | { |
110: | $requestId = UuidGenerator::generatedUuid(); |
111: | $bodyToLog = is_string($body) ? $body : '<binary content>'; |
112: | $this->logRequest($requestId, 'POST', $requestUri, $requestHeaders, $bodyToLog); |
113: | try { |
114: | $response = $this->executeRequest('POST', $requestUri, $requestHeaders, $body, $responseHandler); |
115: | if ($response) { |
116: | $this->logResponse($requestId, $requestUri, $response); |
117: | } |
118: | } catch (Exception $exception) { |
119: | $this->logException($requestId, $requestUri, $exception); |
120: | throw $exception; |
121: | } |
122: | } |
123: | |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | public function put($requestUri, $requestHeaders, $body, callable $responseHandler) |
131: | { |
132: | $requestId = UuidGenerator::generatedUuid(); |
133: | $bodyToLog = is_string($body) ? $body : '<binary content>'; |
134: | $this->logRequest($requestId, 'PUT', $requestUri, $requestHeaders, $bodyToLog); |
135: | try { |
136: | $response = $this->executeRequest('PUT', $requestUri, $requestHeaders, $body, $responseHandler); |
137: | if ($response) { |
138: | $this->logResponse($requestId, $requestUri, $response); |
139: | } |
140: | } catch (Exception $exception) { |
141: | $this->logException($requestId, $requestUri, $exception); |
142: | throw $exception; |
143: | } |
144: | } |
145: | |
146: | |
147: | |
148: | |
149: | public function enableLogging(CommunicatorLogger $communicatorLogger) |
150: | { |
151: | $this->communicatorLogger = $communicatorLogger; |
152: | } |
153: | |
154: | |
155: | |
156: | |
157: | public function disableLogging() |
158: | { |
159: | $this->communicatorLogger = null; |
160: | } |
161: | |
162: | |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | protected function executeRequest( |
171: | $httpMethod, |
172: | $requestUri, |
173: | $requestHeaders, |
174: | $body, |
175: | callable $responseHandler |
176: | ) |
177: | { |
178: | if (!in_array($httpMethod, array('GET', 'DELETE', 'POST', 'PUT'))) { |
179: | throw new UnexpectedValueException(sprintf('Http method \'%s\' is not supported', $httpMethod)); |
180: | } |
181: | $curlHandle = $this->getCurlHandle(); |
182: | $this->setCurlOptions($curlHandle, $httpMethod, $requestUri, $requestHeaders, $body); |
183: | return $this->executeCurlHandle($curlHandle, $responseHandler); |
184: | } |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | protected function getCurlHandle() |
191: | { |
192: | |
193: | if (!$curlHandle = curl_init()) { |
194: | throw new ErrorException('Cannot initialize cUrl curlHandle'); |
195: | } |
196: | return $curlHandle; |
197: | } |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | private function executeCurlHandleShared($multiHandle, $curlHandle) |
205: | { |
206: | $running = 0; |
207: | do { |
208: | $status = curl_multi_exec($multiHandle, $running); |
209: | if ($status > CURLM_OK) { |
210: | $errorMessage = 'cURL error ' . $status; |
211: | if (function_exists('curl_multi_strerror')) { |
212: | $errorMessage .= ' (' . curl_multi_strerror($status) . ')'; |
213: | } |
214: | throw new ErrorException($errorMessage); |
215: | } |
216: | $info = curl_multi_info_read($multiHandle); |
217: | if ($info && isset($info['result']) && $info['result'] != CURLE_OK) { |
218: | $errorMessage = 'cURL error ' . $info['result']; |
219: | if (function_exists('curl_strerror')) { |
220: | $errorMessage .= ' (' . curl_strerror($info['result']) . ')'; |
221: | } |
222: | throw new ErrorException($errorMessage); |
223: | } |
224: | curl_multi_select($multiHandle); |
225: | } while ($running > 0); |
226: | } |
227: | |
228: | |
229: | |
230: | |
231: | |
232: | |
233: | |
234: | private function executeCurlHandle($curlHandle, callable $responseHandler) |
235: | { |
236: | $multiHandle = $this->getCurlMultiHandle(); |
237: | curl_multi_add_handle($multiHandle, $curlHandle); |
238: | |
239: | $headerBuilder = new ResponseHeaderBuilder(); |
240: | $headerFunction = function ($ch, $data) use ($headerBuilder) { |
241: | $headerBuilder->append($data); |
242: | return strlen($data); |
243: | }; |
244: | |
245: | $responseBuilder = $this->communicatorLogger ? new ResponseBuilder() : null; |
246: | $writeFunction = function ($ch, $data) use ($headerBuilder, $responseBuilder, $responseHandler) { |
247: | $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
248: | $headers = $headerBuilder->getHeaders(); |
249: | call_user_func($responseHandler, $httpStatusCode, $data, $headers); |
250: | if ($responseBuilder) { |
251: | $responseBuilder->setHttpStatusCode($httpStatusCode); |
252: | $responseBuilder->setHeaders($headers); |
253: | if ($this->isBinaryResponse($headerBuilder)) { |
254: | $responseBuilder->setBody('<binary content>'); |
255: | } else { |
256: | $responseBuilder->appendBody($data); |
257: | } |
258: | } |
259: | return strlen($data); |
260: | }; |
261: | |
262: | curl_setopt($curlHandle, CURLOPT_HEADERFUNCTION, $headerFunction); |
263: | curl_setopt($curlHandle, CURLOPT_WRITEFUNCTION, $writeFunction); |
264: | |
265: | try { |
266: | $this->executeCurlHandleShared($multiHandle, $curlHandle); |
267: | |
268: | |
269: | |
270: | call_user_func($writeFunction, $curlHandle, ''); |
271: | |
272: | curl_multi_remove_handle($multiHandle, $curlHandle); |
273: | |
274: | return $responseBuilder ? $responseBuilder->getResponse() : null; |
275: | } catch (Exception $e) { |
276: | curl_multi_remove_handle($multiHandle, $curlHandle); |
277: | |
278: | throw $e; |
279: | } |
280: | } |
281: | |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | protected function setCurlOptions( |
290: | $curlHandle, |
291: | $httpMethod, |
292: | $requestUri, |
293: | $requestHeaders, |
294: | $body |
295: | ) |
296: | { |
297: | if (!is_array($requestHeaders)) { |
298: | throw new UnexpectedValueException('Invalid request headers; expected array'); |
299: | } |
300: | curl_setopt($curlHandle, CURLOPT_HEADER, false); |
301: | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); |
302: | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $httpMethod); |
303: | curl_setopt($curlHandle, CURLOPT_URL, $requestUri); |
304: | |
305: | if ($this->connectTimeout > 0) { |
306: | curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout); |
307: | } |
308: | if ($this->readTimeout > 0) { |
309: | curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->readTimeout); |
310: | } |
311: | |
312: | if (in_array($httpMethod, array('PUT', 'POST')) && $body) { |
313: | if (is_string($body)) { |
314: | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); |
315: | } elseif ($body instanceof MultipartFormDataObject) { |
316: | $multipart = new MultipartFormData($body->getBoundary()); |
317: | foreach ($body->getValues() as $name => $value) { |
318: | $multipart->addValue($name, $value); |
319: | } |
320: | foreach ($body->getFiles() as $name => $file) { |
321: | $multipart->addFile($name, $file->getFileName(), $file->getContent(), $file->getContentType(), $file->getContentLength()); |
322: | } |
323: | $multipart->finish(); |
324: | |
325: | $contentLength = $multipart->getContentLength(); |
326: | if ($contentLength >= 0) { |
327: | $requestHeaders[] = 'Content-Length: ' . $contentLength; |
328: | } |
329: | curl_setopt($curlHandle, CURLOPT_READFUNCTION, array($multipart, 'curl_read')); |
330: | curl_setopt($curlHandle, CURLOPT_UPLOAD, true); |
331: | } else { |
332: | $type = is_object($body) ? get_class($body) : gettype($body); |
333: | throw new UnexpectedValueException('Unsupported body type: ' . $type); |
334: | } |
335: | } |
336: | |
337: | if (!empty($requestHeaders)) { |
338: | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, HttpHeaderHelper::generateRawHeaders($requestHeaders)); |
339: | } |
340: | |
341: | if (!is_null($this->proxyConfiguration)) { |
342: | $curlProxy = $this->proxyConfiguration->getCurlProxy(); |
343: | if (!empty($curlProxy)) { |
344: | curl_setopt($curlHandle, CURLOPT_PROXY, $curlProxy); |
345: | } |
346: | $curlProxyUserPwd = $this->proxyConfiguration->getCurlProxyUserPwd(); |
347: | if (!empty($curlProxyUserPwd)) { |
348: | curl_setopt($curlHandle, CURLOPT_PROXYUSERPWD, $curlProxyUserPwd); |
349: | } |
350: | } |
351: | } |
352: | |
353: | |
354: | |
355: | |
356: | |
357: | private function getCurlMultiHandle() |
358: | { |
359: | if (is_null($this->multiHandle)) { |
360: | $multiHandle = curl_multi_init(); |
361: | if ($multiHandle === false) { |
362: | throw new ErrorException('Failed to initialize cURL multi curlHandle'); |
363: | } |
364: | $this->multiHandle = $multiHandle; |
365: | } |
366: | return $this->multiHandle; |
367: | } |
368: | |
369: | |
370: | |
371: | |
372: | private function isBinaryResponse($headerBuilder) |
373: | { |
374: | $contentType = $headerBuilder->getContentType(); |
375: | return $contentType |
376: | |
377: | && strrpos($contentType, 'text/', -strlen($contentType)) === false |
378: | |
379: | && strrpos($contentType, 'json') === false |
380: | |
381: | && strrpos($contentType, 'xml') === false; |
382: | } |
383: | |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | |
390: | |
391: | protected function logRequest($requestId, $requestMethod, $requestUri, array $requestHeaders, $requestBody = '') |
392: | { |
393: | if ($this->communicatorLogger) { |
394: | $this->getCommunicatorLoggerHelper()->logRequest( |
395: | $this->communicatorLogger, |
396: | $requestId, |
397: | $requestMethod, |
398: | $requestUri, |
399: | $requestHeaders, |
400: | $requestBody |
401: | ); |
402: | } |
403: | } |
404: | |
405: | |
406: | |
407: | |
408: | |
409: | |
410: | protected function logResponse($requestId, $requestUri, ConnectionResponse $response) |
411: | { |
412: | if ($this->communicatorLogger) { |
413: | $this->getCommunicatorLoggerHelper()->logResponse( |
414: | $this->communicatorLogger, |
415: | $requestId, |
416: | $requestUri, |
417: | $response |
418: | ); |
419: | } |
420: | } |
421: | |
422: | |
423: | |
424: | |
425: | |
426: | |
427: | protected function logException($requestId, $requestUri, Exception $exception) |
428: | { |
429: | if ($this->communicatorLogger) { |
430: | $this->getCommunicatorLoggerHelper()->logException( |
431: | $this->communicatorLogger, |
432: | $requestId, |
433: | $requestUri, |
434: | $exception |
435: | ); |
436: | } |
437: | } |
438: | |
439: | |
440: | protected function getCommunicatorLoggerHelper() |
441: | { |
442: | if (is_null($this->communicatorLoggerHelper)) { |
443: | $this->communicatorLoggerHelper = new CommunicatorLoggerHelper; |
444: | } |
445: | return $this->communicatorLoggerHelper; |
446: | } |
447: | |
448: | |
449: | |
450: | |
451: | public function setBodyObfuscator(BodyObfuscator $bodyObfuscator) |
452: | { |
453: | $this->getCommunicatorLoggerHelper()->setBodyObfuscator($bodyObfuscator); |
454: | } |
455: | |
456: | |
457: | |
458: | |
459: | public function setHeaderObfuscator(HeaderObfuscator $headerObfuscator) |
460: | { |
461: | $this->getCommunicatorLoggerHelper()->setHeaderObfuscator($headerObfuscator); |
462: | } |
463: | } |
464: | |