Completed
Push — master ( 955097...f4414e )
by Jan
08:34
created

ApiClient::logRequest()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 4.0218

Importance

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