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: * Class ApiException
16: *
17: * @package Worldline\Connect\Sdk\Communication
18: */
19: class DefaultConnection implements Connection
20: {
21: /** @var null|resource */
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 */
37: private $proxyConfiguration = null;
38:
39: /**
40: * @param CommunicatorConfiguration $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: * @throws ErrorException
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: * @return resource
188: * @throws ErrorException
189: */
190: protected function getCurlHandle()
191: {
192: // @phpstan-ignore-next-line
193: if (!$curlHandle = curl_init()) {
194: throw new ErrorException('Cannot initialize cUrl curlHandle');
195: }
196: return $curlHandle;
197: }
198:
199: /**
200: * @param resource $multiHandle
201: * @param resource $curlHandle
202: * @throws ErrorException
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: * @param resource $curlHandle
230: * @param callable $responseHandler
231: * @return ConnectionResponse|null
232: * @throws Exception
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: // always emit an empty chunk, to make sure that the status code and headers are sent,
269: // even if there is no response body
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: * @param resource $curlHandle
284: * @param string $httpMethod
285: * @param string $requestUri
286: * @param string[] $requestHeaders
287: * @param string|MultipartFormDataObject $body
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: * @return resource
355: * @throws Exception
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: * @return bool
371: */
372: private function isBinaryResponse($headerBuilder)
373: {
374: $contentType = $headerBuilder->getContentType();
375: return $contentType
376: // does not start with text/
377: && strrpos($contentType, 'text/', -strlen($contentType)) === false
378: // does not contain json
379: && strrpos($contentType, 'json') === false
380: // does not contain xml
381: && strrpos($contentType, 'xml') === false;
382: }
383:
384: /**
385: * @param string $requestId
386: * @param string $requestMethod
387: * @param string $requestUri
388: * @param array $requestHeaders
389: * @param string $requestBody
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: * @param string $requestId
407: * @param string $requestUri
408: * @param ConnectionResponse $response
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: * @param string $requestId
424: * @param string $requestUri
425: * @param Exception $exception
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: /** @return CommunicatorLoggerHelper */
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: * @param BodyObfuscator $bodyObfuscator
450: */
451: public function setBodyObfuscator(BodyObfuscator $bodyObfuscator)
452: {
453: $this->getCommunicatorLoggerHelper()->setBodyObfuscator($bodyObfuscator);
454: }
455:
456: /**
457: * @param HeaderObfuscator $headerObfuscator
458: */
459: public function setHeaderObfuscator(HeaderObfuscator $headerObfuscator)
460: {
461: $this->getCommunicatorLoggerHelper()->setHeaderObfuscator($headerObfuscator);
462: }
463: }
464: