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: * Class ApiException
16: *
17: * @package Worldline\Acquiring\Sdk\Communication
18: */
19: class DefaultConnection implements Connection
20: {
21: /** @var resource|null */
22: protected $multiHandle = null;
23:
24: /** @var CommunicatorLogger|null */
25: protected $communicatorLogger = null;
26:
27: /** @var CommunicatorLoggerHelper|null */
28: private $communicatorLoggerHelper = null;
29:
30: /** @var int */
31: private $connectTimeout = -1;
32:
33: /** @var int */
34: private $readTimeout = -1;
35:
36: /** @var ProxyConfiguration|null */
37: private $proxyConfiguration = null;
38:
39: /**
40: * @param CommunicatorConfiguration|null $communicatorConfiguration
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: * @param string $requestUri
64: * @param string[] $requestHeaders
65: * @param callable $responseHandler Callable accepting the response status code, a response body chunk and the response headers
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: * @param string $requestUri
84: * @param string[] $requestHeaders
85: * @param callable $responseHandler Callable accepting the response status code, a response body chunk and the response headers
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: * @param string $requestUri
104: * @param string[] $requestHeaders
105: * @param string|MultipartFormDataObject $body
106: * @param callable $responseHandler Callable accepting the response status code, a response body chunk and the response headers
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: * @param string $requestUri
126: * @param string[] $requestHeaders
127: * @param string $body
128: * @param callable $responseHandler Callable accepting the response status code, a response body chunk and the response headers
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: * @param CommunicatorLogger $communicatorLogger
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: * @param string $httpMethod
164: * @param string $requestUri
165: * @param string[] $requestHeaders
166: * @param string|MultipartFormDataObject $body
167: * @param callable $responseHandler Callable accepting the response status code, a response body chunk and the response headers
168: * @return ConnectionResponse|null
169: * @throws ErrorException
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: * @return resource
189: * @throws ErrorException
190: */
191: protected function getCurlHandle()
192: {
193: // @phpstan-ignore-next-line
194: if (!$curlHandle = curl_init()) {
195: throw new ErrorException('Cannot initialize cUrl curlHandle');
196: }
197: return $curlHandle;
198: }
199:
200: /**
201: * @param resource $multiHandle
202: * @param resource $curlHandle
203: * @throws ErrorException
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: * @param resource $curlHandle
231: * @param callable $responseHandler
232: * @return ConnectionResponse|null
233: * @throws Exception
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: // always emit an empty chunk, to make sure that the status code and headers are sent,
270: // even if there is no response body
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: * @param resource $curlHandle
285: * @param string $httpMethod
286: * @param string $requestUri
287: * @param string[] $requestHeaders
288: * @param string|MultipartFormDataObject $body
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: * @return resource
356: * @throws Exception
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: * @return bool
372: */
373: private function isBinaryResponse($headerBuilder)
374: {
375: $contentType = $headerBuilder->getContentType();
376: return $contentType
377: // does not start with text/
378: && strrpos($contentType, 'text/', -strlen($contentType)) === false
379: // does not contain json
380: && strrpos($contentType, 'json') === false
381: // does not contain xml
382: && strrpos($contentType, 'xml') === false;
383: }
384:
385: /**
386: * @param string $requestId
387: * @param string $requestMethod
388: * @param string $requestUri
389: * @param array $requestHeaders
390: * @param string $requestBody
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: * @param string $requestId
408: * @param string $requestUri
409: * @param ConnectionResponse $response
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: * @param string $requestId
425: * @param string $requestUri
426: * @param Exception $exception
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: /** @return CommunicatorLoggerHelper */
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: * @param BodyObfuscator $bodyObfuscator
451: */
452: public function setBodyObfuscator(BodyObfuscator $bodyObfuscator)
453: {
454: $this->getCommunicatorLoggerHelper()->setBodyObfuscator($bodyObfuscator);
455: }
456:
457: /**
458: * @param HeaderObfuscator $headerObfuscator
459: */
460: public function setHeaderObfuscator(HeaderObfuscator $headerObfuscator)
461: {
462: $this->getCommunicatorLoggerHelper()->setHeaderObfuscator($headerObfuscator);
463: }
464: }
465: