1: | <?php |
2: | namespace Worldline\Acquiring\Sdk\Communication; |
3: | |
4: | use ErrorException; |
5: | use Exception; |
6: | use Robtimus\Multipart\MultipartFormData; |
7: | use UnexpectedValueException; |
8: | use Worldline\Acquiring\Sdk\CommunicatorConfiguration; |
9: | use Worldline\Acquiring\Sdk\Logging\BodyObfuscator; |
10: | use Worldline\Acquiring\Sdk\Logging\CommunicatorLogger; |
11: | use Worldline\Acquiring\Sdk\Logging\HeaderObfuscator; |
12: | use Worldline\Acquiring\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: | |
171: | protected function executeRequest( |
172: | $httpMethod, |
173: | $requestUri, |
174: | $requestHeaders, |
175: | $body, |
176: | callable $responseHandler |
177: | ) |
178: | { |
179: | if (!in_array($httpMethod, array('GET', 'DELETE', 'POST', 'PUT'))) { |
180: | throw new UnexpectedValueException(sprintf('Http method \'%s\' is not supported', $httpMethod)); |
181: | } |
182: | $curlHandle = $this->getCurlHandle(); |
183: | $this->setCurlOptions($curlHandle, $httpMethod, $requestUri, $requestHeaders, $body); |
184: | return $this->executeCurlHandle($curlHandle, $responseHandler); |
185: | } |
186: | |
187: | |
188: | |
189: | |
190: | |
191: | protected function getCurlHandle() |
192: | { |
193: | |
194: | if (!$curlHandle = curl_init()) { |
195: | throw new ErrorException('Cannot initialize cUrl curlHandle'); |
196: | } |
197: | return $curlHandle; |
198: | } |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | private function executeCurlHandleShared($multiHandle, $curlHandle) |
206: | { |
207: | $running = 0; |
208: | do { |
209: | $status = curl_multi_exec($multiHandle, $running); |
210: | if ($status > CURLM_OK) { |
211: | $errorMessage = 'cURL error ' . $status; |
212: | if (function_exists('curl_multi_strerror')) { |
213: | $errorMessage .= ' (' . curl_multi_strerror($status) . ')'; |
214: | } |
215: | throw new ErrorException($errorMessage); |
216: | } |
217: | $info = curl_multi_info_read($multiHandle); |
218: | if ($info && isset($info['result']) && $info['result'] != CURLE_OK) { |
219: | $errorMessage = 'cURL error ' . $info['result']; |
220: | if (function_exists('curl_strerror')) { |
221: | $errorMessage .= ' (' . curl_strerror($info['result']) . ')'; |
222: | } |
223: | throw new ErrorException($errorMessage); |
224: | } |
225: | curl_multi_select($multiHandle); |
226: | } while ($running > 0); |
227: | } |
228: | |
229: | |
230: | |
231: | |
232: | |
233: | |
234: | |
235: | private function executeCurlHandle($curlHandle, callable $responseHandler) |
236: | { |
237: | $multiHandle = $this->getCurlMultiHandle(); |
238: | curl_multi_add_handle($multiHandle, $curlHandle); |
239: | |
240: | $headerBuilder = new ResponseHeaderBuilder(); |
241: | $headerFunction = function ($ch, $data) use ($headerBuilder) { |
242: | $headerBuilder->append($data); |
243: | return strlen($data); |
244: | }; |
245: | |
246: | $responseBuilder = $this->communicatorLogger ? new ResponseBuilder() : null; |
247: | $writeFunction = function ($ch, $data) use ($headerBuilder, $responseBuilder, $responseHandler) { |
248: | $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); |
249: | $headers = $headerBuilder->getHeaders(); |
250: | call_user_func($responseHandler, $httpStatusCode, $data, $headers); |
251: | if ($responseBuilder) { |
252: | $responseBuilder->setHttpStatusCode($httpStatusCode); |
253: | $responseBuilder->setHeaders($headers); |
254: | if ($this->isBinaryResponse($headerBuilder)) { |
255: | $responseBuilder->setBody('<binary content>'); |
256: | } else { |
257: | $responseBuilder->appendBody($data); |
258: | } |
259: | } |
260: | return strlen($data); |
261: | }; |
262: | |
263: | curl_setopt($curlHandle, CURLOPT_HEADERFUNCTION, $headerFunction); |
264: | curl_setopt($curlHandle, CURLOPT_WRITEFUNCTION, $writeFunction); |
265: | |
266: | try { |
267: | $this->executeCurlHandleShared($multiHandle, $curlHandle); |
268: | |
269: | |
270: | |
271: | call_user_func($writeFunction, $curlHandle, ''); |
272: | |
273: | curl_multi_remove_handle($multiHandle, $curlHandle); |
274: | |
275: | return $responseBuilder ? $responseBuilder->getResponse() : null; |
276: | } catch (Exception $e) { |
277: | curl_multi_remove_handle($multiHandle, $curlHandle); |
278: | |
279: | throw $e; |
280: | } |
281: | } |
282: | |
283: | |
284: | |
285: | |
286: | |
287: | |
288: | |
289: | |
290: | protected function setCurlOptions( |
291: | $curlHandle, |
292: | $httpMethod, |
293: | $requestUri, |
294: | $requestHeaders, |
295: | $body |
296: | ) |
297: | { |
298: | if (!is_array($requestHeaders)) { |
299: | throw new UnexpectedValueException('Invalid request headers; expected array'); |
300: | } |
301: | curl_setopt($curlHandle, CURLOPT_HEADER, false); |
302: | curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); |
303: | curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, $httpMethod); |
304: | curl_setopt($curlHandle, CURLOPT_URL, $requestUri); |
305: | |
306: | if ($this->connectTimeout > 0) { |
307: | curl_setopt($curlHandle, CURLOPT_CONNECTTIMEOUT, $this->connectTimeout); |
308: | } |
309: | if ($this->readTimeout > 0) { |
310: | curl_setopt($curlHandle, CURLOPT_TIMEOUT, $this->readTimeout); |
311: | } |
312: | |
313: | if (in_array($httpMethod, array('PUT', 'POST')) && $body) { |
314: | if (is_string($body)) { |
315: | curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $body); |
316: | } else if ($body instanceof MultipartFormDataObject) { |
317: | $multipart = new MultipartFormData($body->getBoundary()); |
318: | foreach ($body->getValues() as $name => $value) { |
319: | $multipart->addValue($name, $value); |
320: | } |
321: | foreach ($body->getFiles() as $name => $file) { |
322: | $multipart->addFile($name, $file->getFileName(), $file->getContent(), $file->getContentType(), $file->getContentLength()); |
323: | } |
324: | $multipart->finish(); |
325: | |
326: | $contentLength = $multipart->getContentLength(); |
327: | if ($contentLength >= 0) { |
328: | $requestHeaders[] = 'Content-Length: ' . $contentLength; |
329: | } |
330: | curl_setopt($curlHandle, CURLOPT_READFUNCTION, array($multipart, 'curl_read')); |
331: | curl_setopt($curlHandle, CURLOPT_UPLOAD, true); |
332: | } else { |
333: | $type = is_object($body) ? get_class($body) : gettype($body); |
334: | throw new UnexpectedValueException('Unsupported body type: ' . $type); |
335: | } |
336: | } |
337: | |
338: | if (count($requestHeaders) > 0) { |
339: | curl_setopt($curlHandle, CURLOPT_HTTPHEADER, HttpHeaderHelper::generateRawHeaders($requestHeaders)); |
340: | } |
341: | |
342: | if (!is_null($this->proxyConfiguration)) { |
343: | $curlProxy = $this->proxyConfiguration->getCurlProxy(); |
344: | if (!empty($curlProxy)) { |
345: | curl_setopt($curlHandle, CURLOPT_PROXY, $curlProxy); |
346: | } |
347: | $curlProxyUserPwd = $this->proxyConfiguration->getCurlProxyUserPwd(); |
348: | if (!empty($curlProxyUserPwd)) { |
349: | curl_setopt($curlHandle, CURLOPT_PROXYUSERPWD, $curlProxyUserPwd); |
350: | } |
351: | } |
352: | } |
353: | |
354: | |
355: | |
356: | |
357: | |
358: | private function getCurlMultiHandle() |
359: | { |
360: | if (is_null($this->multiHandle)) { |
361: | $multiHandle = curl_multi_init(); |
362: | if ($multiHandle === false) { |
363: | throw new Exception('Failed to initialize cURL multi curlHandle'); |
364: | } |
365: | $this->multiHandle = $multiHandle; |
366: | } |
367: | return $this->multiHandle; |
368: | } |
369: | |
370: | |
371: | |
372: | |
373: | private function isBinaryResponse($headerBuilder) |
374: | { |
375: | $contentType = $headerBuilder->getContentType(); |
376: | return $contentType |
377: | |
378: | && strrpos($contentType, 'text/', -strlen($contentType)) === false |
379: | |
380: | && strrpos($contentType, 'json') === false |
381: | |
382: | && strrpos($contentType, 'xml') === false; |
383: | } |
384: | |
385: | |
386: | |
387: | |
388: | |
389: | |
390: | |
391: | |
392: | protected function logRequest($requestId, $requestMethod, $requestUri, array $requestHeaders, $requestBody = '') |
393: | { |
394: | if ($this->communicatorLogger) { |
395: | $this->getCommunicatorLoggerHelper()->logRequest( |
396: | $this->communicatorLogger, |
397: | $requestId, |
398: | $requestMethod, |
399: | $requestUri, |
400: | $requestHeaders, |
401: | $requestBody |
402: | ); |
403: | } |
404: | } |
405: | |
406: | |
407: | |
408: | |
409: | |
410: | |
411: | protected function logResponse($requestId, $requestUri, ConnectionResponse $response) |
412: | { |
413: | if ($this->communicatorLogger) { |
414: | $this->getCommunicatorLoggerHelper()->logResponse( |
415: | $this->communicatorLogger, |
416: | $requestId, |
417: | $requestUri, |
418: | $response |
419: | ); |
420: | } |
421: | } |
422: | |
423: | |
424: | |
425: | |
426: | |
427: | |
428: | protected function logException($requestId, $requestUri, Exception $exception) |
429: | { |
430: | if ($this->communicatorLogger) { |
431: | $this->getCommunicatorLoggerHelper()->logException( |
432: | $this->communicatorLogger, |
433: | $requestId, |
434: | $requestUri, |
435: | $exception |
436: | ); |
437: | } |
438: | } |
439: | |
440: | |
441: | protected function getCommunicatorLoggerHelper() |
442: | { |
443: | if (is_null($this->communicatorLoggerHelper)) { |
444: | $this->communicatorLoggerHelper = new CommunicatorLoggerHelper; |
445: | } |
446: | return $this->communicatorLoggerHelper; |
447: | } |
448: | |
449: | |
450: | |
451: | |
452: | public function setBodyObfuscator(BodyObfuscator $bodyObfuscator) |
453: | { |
454: | $this->getCommunicatorLoggerHelper()->setBodyObfuscator($bodyObfuscator); |
455: | } |
456: | |
457: | |
458: | |
459: | |
460: | public function setHeaderObfuscator(HeaderObfuscator $headerObfuscator) |
461: | { |
462: | $this->getCommunicatorLoggerHelper()->setHeaderObfuscator($headerObfuscator); |
463: | } |
464: | } |
465: | |