Completed
Pull Request — master (#15)
by Jan
04:04
created

ApiClient::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 4
rs 10
c 0
b 0
f 0
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
	public function __construct(
39
		ApiClientDriver $driver,
40
		CryptoService $cryptoService,
41
		string $apiUrl = null
42
	)
43
	{
44
		$this->driver = $driver;
45
		$this->cryptoService = $cryptoService;
46
		$this->apiUrl = $apiUrl;
47
	}
48
49
	public function setLogger(LoggerInterface $logger = null)
50
	{
51
		$this->logger = $logger;
52
	}
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 View Code Duplication
	public function get(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
72
		string $url,
73
		array $data = [],
74
		SignatureDataFormatter $requestSignatureDataFormatter,
75
		SignatureDataFormatter $responseSignatureDataFormatter,
76
		\Closure $responseValidityCallback = null,
77
		array $extensions = []
78
	): Response
79
	{
80
		return $this->request(
81
			new HttpMethod(HttpMethod::GET),
82
			$url,
83
			$this->prepareData($data, $requestSignatureDataFormatter),
84
			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 View Code Duplication
	public function post(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
108
		string $url,
109
		array $data = [],
110
		SignatureDataFormatter $requestSignatureDataFormatter,
111
		SignatureDataFormatter $responseSignatureDataFormatter,
112
		array $extensions = []
113
	): Response
114
	{
115
		return $this->request(
116
			new HttpMethod(HttpMethod::POST),
117
			$url,
118
			[],
119
			$this->prepareData($data, $requestSignatureDataFormatter),
120
			$responseSignatureDataFormatter,
121
			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 View Code Duplication
	public function put(
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
143
		string $url,
144
		array $data = [],
145
		SignatureDataFormatter $requestSignatureDataFormatter,
146
		SignatureDataFormatter $responseSignatureDataFormatter,
147
		array $extensions = []
148
	): Response
149
	{
150
		return $this->request(
151
			new HttpMethod(HttpMethod::PUT),
152
			$url,
153
			[],
154
			$this->prepareData($data, $requestSignatureDataFormatter),
155
			$responseSignatureDataFormatter,
156
			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
	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
		$urlFirstQueryPosition = strpos($url, '{');
190
		$endpointName = ($urlFirstQueryPosition !== false ? substr($url, 0, $urlFirstQueryPosition - 1) : $url);
191
		$originalQueries = $queries;
192
193
		foreach ($queries as $key => $value) {
194
			if (strpos($url, '{' . $key . '}') !== false) {
195
				$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $url. This often makes code more readable.
Loading history...
196
				unset($queries[$key]);
197
			}
198
		}
199
200
		if ($queries !== []) {
201
			throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
202
		}
203
204
		$response = $this->driver->request(
205
			$method,
206
			$this->apiUrl . '/' . $url,
207
			$data
208
		);
209
210
		$this->logRequest($method, $endpointName, $originalQueries, $data, $response);
211
212
		if ($responseValidityCallback !== null) {
213
			$responseValidityCallback($response);
214
		}
215
216
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
217
			$decodedExtensions = [];
218
			if ($extensions !== [] && array_key_exists('extensions', $response->getData())) {
219
				foreach ($response->getData()['extensions'] as $extensionData) {
220
					$name = $extensionData['extension'];
221
					if (isset($extensions[$name])) {
222
						$handler = $extensions[$name];
223
						$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
224
					}
225
				}
226
			}
227
			$responseData = $this->decodeData($response->getData(), $responseSignatureDataFormatter);
228
			unset($responseData['extensions']);
229
230
			return new Response(
231
				$response->getResponseCode(),
232
				$responseData,
233
				$response->getHeaders(),
234
				$decodedExtensions
235
			);
236
237
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
238
			return new Response(
239
				$response->getResponseCode(),
240
				null,
241
				$response->getHeaders()
242
			);
243
244
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
245
			throw new BadRequestException($response);
246
247
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
248
			throw new ForbiddenException($response);
249
250
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
251
			throw new NotFoundException($response);
252
253
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
254
			throw new MethodNotAllowedException($response);
255
256
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
257
			throw new TooManyRequestsException($response);
258
259
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
260
			throw new ServiceUnavailableException($response);
261
		}
262
263
		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
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
278
	{
279
		$response = new Response(
280
			new ResponseCode(ResponseCode::S200_OK),
281
			$data
282
		);
283
284
		$this->logRequest(new HttpMethod(HttpMethod::GET), 'payment/response', [], [], $response);
285
286
		return new Response(
287
			$response->getResponseCode(),
288
			$this->decodeData($data, $responseSignatureDataFormatter),
289
			$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
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
302
	{
303
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
304
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
305
306
		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
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
319
	{
320
		if (!array_key_exists('signature', $responseData)) {
321
			throw new InvalidSignatureException($responseData);
322
		}
323
324
		$signature = $responseData['signature'];
325
		unset($responseData['signature']);
326
327
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
328
			throw new InvalidSignatureException($responseData);
329
		}
330
331
		return $responseData;
332
	}
333
334
	private function logRequest(HttpMethod $method, string $url, array $queries, array $requestData = null, Response $response)
335
	{
336
		if ($this->logger === null) {
337
			return;
338
		}
339
340
		$responseData = $response->getData();
341
342
		unset($requestData['signature']);
343
		unset($queries['signature']);
344
		unset($responseData['signature']);
345
346
		if (isset($responseData['extensions'])) {
347
			foreach ($responseData['extensions'] as $key => $extensionData) {
348
				unset($responseData['extensions'][$key]['signature']);
349
			}
350
		}
351
		$context = [
352
			'request' => [
353
				'method' => $method->getValue(),
354
				'queries' => $queries,
355
				'data' => $requestData,
356
			],
357
			'response' => [
358
				'code' => $response->getResponseCode()->getValue(),
359
				'data' => $responseData,
360
			],
361
		];
362
363
		$this->logger->info($url, $context);
364
	}
365
366
}
367