1
|
|
|
<?php declare(strict_types = 1); |
2
|
|
|
|
3
|
|
|
namespace SlevomatCsobGateway\Api; |
4
|
|
|
|
5
|
|
|
use DateTimeImmutable; |
6
|
|
|
use Psr\Log\LoggerInterface; |
7
|
|
|
use SlevomatCsobGateway\Call\ResponseExtensionHandler; |
8
|
|
|
use SlevomatCsobGateway\Crypto\CryptoService; |
9
|
|
|
use SlevomatCsobGateway\Crypto\PrivateKeyFileException; |
10
|
|
|
use SlevomatCsobGateway\Crypto\PublicKeyFileException; |
11
|
|
|
use SlevomatCsobGateway\Crypto\SignatureDataFormatter; |
12
|
|
|
use SlevomatCsobGateway\Crypto\SigningFailedException; |
13
|
|
|
use SlevomatCsobGateway\Crypto\VerificationFailedException; |
14
|
|
|
|
15
|
|
|
class ApiClient |
16
|
|
|
{ |
17
|
|
|
|
18
|
|
|
/** |
19
|
|
|
* @var ApiClientDriver |
20
|
|
|
*/ |
21
|
|
|
private $driver; |
22
|
|
|
|
23
|
|
|
/** |
24
|
|
|
* @var CryptoService |
25
|
|
|
*/ |
26
|
|
|
private $cryptoService; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* @var LoggerInterface|null |
30
|
|
|
*/ |
31
|
|
|
private $logger; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var string |
35
|
|
|
*/ |
36
|
|
|
private $apiUrl; |
37
|
|
|
|
38
|
16 |
|
public function __construct( |
39
|
|
|
ApiClientDriver $driver, |
40
|
|
|
CryptoService $cryptoService, |
41
|
|
|
string $apiUrl = null |
42
|
|
|
) |
43
|
|
|
{ |
44
|
16 |
|
$this->driver = $driver; |
45
|
16 |
|
$this->cryptoService = $cryptoService; |
46
|
16 |
|
$this->apiUrl = $apiUrl; |
47
|
16 |
|
} |
48
|
|
|
|
49
|
5 |
|
public function setLogger(LoggerInterface $logger = null) |
50
|
|
|
{ |
51
|
5 |
|
$this->logger = $logger; |
52
|
5 |
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @param string $url |
56
|
|
|
* @param mixed[]|null $data |
57
|
|
|
* @param SignatureDataFormatter $requestSignatureDataFormatter |
58
|
|
|
* @param SignatureDataFormatter $responseSignatureDataFormatter |
59
|
|
|
* @param \Closure|null $responseValidityCallback |
60
|
|
|
* @param ResponseExtensionHandler[] $extensions |
61
|
|
|
* @return Response |
62
|
|
|
* |
63
|
|
|
* @throws PrivateKeyFileException |
64
|
|
|
* @throws SigningFailedException |
65
|
|
|
* @throws PublicKeyFileException |
66
|
|
|
* @throws VerificationFailedException |
67
|
|
|
* @throws RequestException |
68
|
|
|
* @throws ApiClientDriverException |
69
|
|
|
* @throws InvalidSignatureException |
70
|
|
|
*/ |
71
|
13 |
View Code Duplication |
public function get( |
|
|
|
|
72
|
|
|
string $url, |
73
|
|
|
array $data = [], |
74
|
|
|
SignatureDataFormatter $requestSignatureDataFormatter, |
75
|
|
|
SignatureDataFormatter $responseSignatureDataFormatter, |
76
|
|
|
\Closure $responseValidityCallback = null, |
77
|
|
|
array $extensions = [] |
78
|
|
|
): Response |
79
|
|
|
{ |
80
|
13 |
|
return $this->request( |
81
|
13 |
|
new HttpMethod(HttpMethod::GET), |
82
|
|
|
$url, |
83
|
13 |
|
$this->prepareData($data, $requestSignatureDataFormatter), |
84
|
13 |
|
null, |
85
|
|
|
$responseSignatureDataFormatter, |
86
|
|
|
$responseValidityCallback, |
87
|
|
|
$extensions |
88
|
|
|
); |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
/** |
92
|
|
|
* @param string $url |
93
|
|
|
* @param mixed[]|null $data |
94
|
|
|
* @param SignatureDataFormatter $requestSignatureDataFormatter |
95
|
|
|
* @param SignatureDataFormatter $responseSignatureDataFormatter |
96
|
|
|
* @param ResponseExtensionHandler[] $extensions |
97
|
|
|
* @return Response |
98
|
|
|
* |
99
|
|
|
* @throws PrivateKeyFileException |
100
|
|
|
* @throws SigningFailedException |
101
|
|
|
* @throws PublicKeyFileException |
102
|
|
|
* @throws VerificationFailedException |
103
|
|
|
* @throws RequestException |
104
|
|
|
* @throws ApiClientDriverException |
105
|
|
|
* @throws InvalidSignatureException |
106
|
|
|
*/ |
107
|
1 |
View Code Duplication |
public function post( |
|
|
|
|
108
|
|
|
string $url, |
109
|
|
|
array $data = [], |
110
|
|
|
SignatureDataFormatter $requestSignatureDataFormatter, |
111
|
|
|
SignatureDataFormatter $responseSignatureDataFormatter, |
112
|
|
|
array $extensions = [] |
113
|
|
|
): Response |
114
|
|
|
{ |
115
|
1 |
|
return $this->request( |
116
|
1 |
|
new HttpMethod(HttpMethod::POST), |
117
|
|
|
$url, |
118
|
1 |
|
[], |
119
|
1 |
|
$this->prepareData($data, $requestSignatureDataFormatter), |
120
|
|
|
$responseSignatureDataFormatter, |
121
|
1 |
|
null, |
122
|
|
|
$extensions |
123
|
|
|
); |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* @param string $url |
128
|
|
|
* @param mixed[]|null $data |
129
|
|
|
* @param SignatureDataFormatter $requestSignatureDataFormatter |
130
|
|
|
* @param SignatureDataFormatter $responseSignatureDataFormatter |
131
|
|
|
* @param ResponseExtensionHandler[] $extensions |
132
|
|
|
* @return Response |
133
|
|
|
* |
134
|
|
|
* @throws PrivateKeyFileException |
135
|
|
|
* @throws SigningFailedException |
136
|
|
|
* @throws PublicKeyFileException |
137
|
|
|
* @throws VerificationFailedException |
138
|
|
|
* @throws RequestException |
139
|
|
|
* @throws ApiClientDriverException |
140
|
|
|
* @throws InvalidSignatureException |
141
|
|
|
*/ |
142
|
1 |
View Code Duplication |
public function put( |
|
|
|
|
143
|
|
|
string $url, |
144
|
|
|
array $data = [], |
145
|
|
|
SignatureDataFormatter $requestSignatureDataFormatter, |
146
|
|
|
SignatureDataFormatter $responseSignatureDataFormatter, |
147
|
|
|
array $extensions = [] |
148
|
|
|
): Response |
149
|
|
|
{ |
150
|
1 |
|
return $this->request( |
151
|
1 |
|
new HttpMethod(HttpMethod::PUT), |
152
|
|
|
$url, |
153
|
1 |
|
[], |
154
|
1 |
|
$this->prepareData($data, $requestSignatureDataFormatter), |
155
|
|
|
$responseSignatureDataFormatter, |
156
|
1 |
|
null, |
157
|
|
|
$extensions |
158
|
|
|
); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* @param HttpMethod $method |
163
|
|
|
* @param string $url |
164
|
|
|
* @param string[] $queries |
165
|
|
|
* @param mixed[]|null $data |
166
|
|
|
* @param SignatureDataFormatter $responseSignatureDataFormatter |
167
|
|
|
* @param \Closure|null $responseValidityCallback |
168
|
|
|
* @param ResponseExtensionHandler[] $extensions |
169
|
|
|
* @return Response |
170
|
|
|
* |
171
|
|
|
* @throws PrivateKeyFileException |
172
|
|
|
* @throws SigningFailedException |
173
|
|
|
* @throws PublicKeyFileException |
174
|
|
|
* @throws VerificationFailedException |
175
|
|
|
* @throws RequestException |
176
|
|
|
* @throws ApiClientDriverException |
177
|
|
|
* @throws InvalidSignatureException |
178
|
|
|
*/ |
179
|
15 |
|
public function request( |
180
|
|
|
HttpMethod $method, |
181
|
|
|
string $url, |
182
|
|
|
array $queries = [], |
183
|
|
|
array $data = null, |
184
|
|
|
SignatureDataFormatter $responseSignatureDataFormatter, |
185
|
|
|
\Closure $responseValidityCallback = null, |
186
|
|
|
array $extensions = [] |
187
|
|
|
): Response |
188
|
|
|
{ |
189
|
15 |
|
$urlFirstQueryPosition = strpos($url, '{'); |
190
|
15 |
|
$endpointName = ($urlFirstQueryPosition !== false ? substr($url, 0, $urlFirstQueryPosition - 1) : $url); |
191
|
15 |
|
$originalQueries = $queries; |
192
|
|
|
|
193
|
15 |
|
foreach ($queries as $key => $value) { |
194
|
13 |
|
if (strpos($url, '{' . $key . '}') !== false) { |
195
|
13 |
|
$url = str_replace('{' . $key . '}', urlencode((string) $value), $url); |
|
|
|
|
196
|
13 |
|
unset($queries[$key]); |
197
|
|
|
} |
198
|
|
|
} |
199
|
|
|
|
200
|
15 |
|
if ($queries !== []) { |
201
|
|
|
throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries)); |
202
|
|
|
} |
203
|
|
|
|
204
|
15 |
|
$response = $this->driver->request( |
205
|
|
|
$method, |
206
|
15 |
|
$this->apiUrl . '/' . $url, |
207
|
|
|
$data |
208
|
|
|
); |
209
|
|
|
|
210
|
15 |
|
$this->logRequest($method, $endpointName, $originalQueries, $data, $response); |
211
|
|
|
|
212
|
15 |
|
if ($responseValidityCallback !== null) { |
213
|
|
|
$responseValidityCallback($response); |
214
|
|
|
} |
215
|
|
|
|
216
|
15 |
|
if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) { |
217
|
7 |
|
$decodedExtensions = []; |
218
|
7 |
|
if ($extensions !== [] && array_key_exists('extensions', $response->getData())) { |
219
|
1 |
|
foreach ($response->getData()['extensions'] as $extensionData) { |
220
|
1 |
|
$name = $extensionData['extension']; |
221
|
1 |
|
if (isset($extensions[$name])) { |
222
|
1 |
|
$handler = $extensions[$name]; |
223
|
1 |
|
$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter())); |
224
|
|
|
} |
225
|
|
|
} |
226
|
|
|
} |
227
|
7 |
|
$responseData = $this->decodeData($response->getData(), $responseSignatureDataFormatter); |
228
|
5 |
|
unset($responseData['extensions']); |
229
|
|
|
|
230
|
5 |
|
return new Response( |
231
|
5 |
|
$response->getResponseCode(), |
232
|
|
|
$responseData, |
233
|
5 |
|
$response->getHeaders(), |
234
|
|
|
$decodedExtensions |
235
|
|
|
); |
236
|
|
|
|
237
|
8 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) { |
238
|
1 |
|
return new Response( |
239
|
1 |
|
$response->getResponseCode(), |
240
|
1 |
|
null, |
241
|
1 |
|
$response->getHeaders() |
242
|
|
|
); |
243
|
|
|
|
244
|
7 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) { |
245
|
1 |
|
throw new BadRequestException($response); |
246
|
|
|
|
247
|
6 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) { |
248
|
1 |
|
throw new ForbiddenException($response); |
249
|
|
|
|
250
|
5 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) { |
251
|
1 |
|
throw new NotFoundException($response); |
252
|
|
|
|
253
|
4 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) { |
254
|
1 |
|
throw new MethodNotAllowedException($response); |
255
|
|
|
|
256
|
3 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) { |
257
|
1 |
|
throw new TooManyRequestsException($response); |
258
|
|
|
|
259
|
2 |
|
} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) { |
260
|
1 |
|
throw new ServiceUnavailableException($response); |
261
|
|
|
} |
262
|
|
|
|
263
|
1 |
|
throw new InternalErrorException($response); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @param mixed[] $data |
268
|
|
|
* @param SignatureDataFormatter $responseSignatureDataFormatter |
269
|
|
|
* @return Response |
270
|
|
|
* |
271
|
|
|
* @throws InvalidSignatureException |
272
|
|
|
* @throws PrivateKeyFileException |
273
|
|
|
* @throws SigningFailedException |
274
|
|
|
* @throws PublicKeyFileException |
275
|
|
|
* @throws VerificationFailedException |
276
|
|
|
*/ |
277
|
1 |
|
public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response |
278
|
|
|
{ |
279
|
1 |
|
$response = new Response( |
280
|
1 |
|
new ResponseCode(ResponseCode::S200_OK), |
281
|
|
|
$data |
282
|
|
|
); |
283
|
|
|
|
284
|
1 |
|
$this->logRequest(new HttpMethod(HttpMethod::GET), 'payment/response', [], [], $response); |
285
|
|
|
|
286
|
1 |
|
return new Response( |
287
|
1 |
|
$response->getResponseCode(), |
288
|
1 |
|
$this->decodeData($data, $responseSignatureDataFormatter), |
289
|
1 |
|
$response->getHeaders() |
290
|
|
|
); |
291
|
|
|
} |
292
|
|
|
|
293
|
|
|
/** |
294
|
|
|
* @param mixed[] $data |
295
|
|
|
* @param SignatureDataFormatter $signatureDataFormatter |
296
|
|
|
* @return mixed[] |
297
|
|
|
* |
298
|
|
|
* @throws PrivateKeyFileException |
299
|
|
|
* @throws SigningFailedException |
300
|
|
|
*/ |
301
|
15 |
|
private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array |
302
|
|
|
{ |
303
|
15 |
|
$data['dttm'] = (new DateTimeImmutable())->format('YmdHis'); |
304
|
15 |
|
$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter); |
305
|
|
|
|
306
|
15 |
|
return $data; |
307
|
|
|
} |
308
|
|
|
|
309
|
|
|
/** |
310
|
|
|
* @param mixed[] $responseData |
311
|
|
|
* @param SignatureDataFormatter $signatureDataFormatter |
312
|
|
|
* @return mixed[] |
313
|
|
|
* |
314
|
|
|
* @throws InvalidSignatureException |
315
|
|
|
* @throws PublicKeyFileException |
316
|
|
|
* @throws VerificationFailedException |
317
|
|
|
*/ |
318
|
8 |
|
private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array |
319
|
|
|
{ |
320
|
8 |
|
if (!array_key_exists('signature', $responseData)) { |
321
|
1 |
|
throw new InvalidSignatureException($responseData); |
322
|
|
|
} |
323
|
|
|
|
324
|
7 |
|
$signature = $responseData['signature']; |
325
|
7 |
|
unset($responseData['signature']); |
326
|
|
|
|
327
|
7 |
|
if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) { |
328
|
1 |
|
throw new InvalidSignatureException($responseData); |
329
|
|
|
} |
330
|
|
|
|
331
|
6 |
|
return $responseData; |
332
|
|
|
} |
333
|
|
|
|
334
|
16 |
|
private function logRequest(HttpMethod $method, string $url, array $queries, array $requestData = null, Response $response) |
335
|
|
|
{ |
336
|
16 |
|
if ($this->logger === null) { |
337
|
11 |
|
return; |
338
|
|
|
} |
339
|
|
|
|
340
|
5 |
|
$responseData = $response->getData(); |
341
|
|
|
|
342
|
5 |
|
unset($requestData['signature']); |
343
|
5 |
|
unset($queries['signature']); |
344
|
5 |
|
unset($responseData['signature']); |
345
|
|
|
|
346
|
5 |
|
if (isset($responseData['extensions'])) { |
347
|
|
|
foreach ($responseData['extensions'] as $key => $extensionData) { |
348
|
|
|
unset($responseData['extensions'][$key]['signature']); |
349
|
|
|
} |
350
|
|
|
} |
351
|
|
|
$context = [ |
352
|
|
|
'request' => [ |
353
|
5 |
|
'method' => $method->getValue(), |
354
|
5 |
|
'queries' => $queries, |
355
|
5 |
|
'data' => $requestData, |
356
|
|
|
], |
357
|
|
|
'response' => [ |
358
|
5 |
|
'code' => $response->getResponseCode()->getValue(), |
359
|
5 |
|
'data' => $responseData, |
360
|
|
|
], |
361
|
|
|
]; |
362
|
|
|
|
363
|
5 |
|
$this->logger->info($url, $context); |
364
|
5 |
|
} |
365
|
|
|
|
366
|
|
|
} |
367
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.