Completed
Pull Request — master (#15)
by
unknown
05:26
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
		foreach ($queries as $key => $value) {
190
			if (strpos($url, '{' . $key . '}') !== false) {
191
				$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
192
				unset($queries[$key]);
193
			}
194
		}
195
196
		if ($queries !== []) {
197
			throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
198
		}
199
200
		$response = $this->driver->request(
201
			$method,
202
			$this->apiUrl . '/' . $url,
203
			$data
204
		);
205
206
		$this->logRequest($url, $queries, $data, $response);
207
208
		if ($responseValidityCallback !== null) {
209
			$responseValidityCallback($response);
210
		}
211
212
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
213
			$decodedExtensions = [];
214
			if ($extensions !== [] && array_key_exists('extensions', $response->getData())) {
215
				foreach ($response->getData()['extensions'] as $extensionData) {
216
					$name = $extensionData['extension'];
217
					if (isset($extensions[$name])) {
218
						$handler = $extensions[$name];
219
						$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
220
					}
221
				}
222
			}
223
			$responseData = $this->decodeData($response->getData(), $responseSignatureDataFormatter);
224
			unset($responseData['extensions']);
225
226
			return new Response(
227
				$response->getResponseCode(),
228
				$responseData,
229
				$response->getHeaders(),
230
				$decodedExtensions
231
			);
232
233
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
234
			return new Response(
235
				$response->getResponseCode(),
236
				null,
237
				$response->getHeaders()
238
			);
239
240
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
241
			throw new BadRequestException($response);
242
243
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
244
			throw new ForbiddenException($response);
245
246
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
247
			throw new NotFoundException($response);
248
249
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
250
			throw new MethodNotAllowedException($response);
251
252
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
253
			throw new TooManyRequestsException($response);
254
255
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
256
			throw new ServiceUnavailableException($response);
257
		}
258
259
		throw new InternalErrorException($response);
260
	}
261
262
	/**
263
	 * @param mixed[] $data
264
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
265
	 * @return Response
266
	 *
267
	 * @throws InvalidSignatureException
268
	 * @throws PrivateKeyFileException
269
	 * @throws SigningFailedException
270
	 * @throws PublicKeyFileException
271
	 * @throws VerificationFailedException
272
	 */
273
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
274
	{
275
		$response = new Response(
276
			new ResponseCode(ResponseCode::S200_OK),
277
			$data
278
		);
279
280
		return new Response(
281
			$response->getResponseCode(),
282
			$this->decodeData($data, $responseSignatureDataFormatter),
283
			$response->getHeaders()
284
		);
285
	}
286
287
	/**
288
	 * @param mixed[] $data
289
	 * @param SignatureDataFormatter $signatureDataFormatter
290
	 * @return mixed[]
291
	 *
292
	 * @throws PrivateKeyFileException
293
	 * @throws SigningFailedException
294
	 */
295
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
296
	{
297
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
298
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
299
300
		return $data;
301
	}
302
303
	/**
304
	 * @param mixed[] $responseData
305
	 * @param SignatureDataFormatter $signatureDataFormatter
306
	 * @return mixed[]
307
	 *
308
	 * @throws InvalidSignatureException
309
	 * @throws PublicKeyFileException
310
	 * @throws VerificationFailedException
311
	 */
312
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
313
	{
314
		if (!array_key_exists('signature', $responseData)) {
315
			throw new InvalidSignatureException($responseData);
316
		}
317
318
		$signature = $responseData['signature'];
319
		unset($responseData['signature']);
320
321
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
322
			throw new InvalidSignatureException($responseData);
323
		}
324
325
		return $responseData;
326
	}
327
328
	private function logRequest(string $url, array $queries, array $requestData = null, Response $response)
329
	{
330
		if ($this->logger === null) {
331
			return;
332
		}
333
334
		$context = [
335
			'type' => 'csobRequest',
336
			'response' => [
337
				'code' => $response->getResponseCode()->getValue(),
338
				'data' => $response->getData(),
339
			],
340
		];
341
342
		if (count($queries) !== 0) {
343
			$context['queries'] = $queries;
344
		}
345
346
		if ($requestData !== null) {
347
			$context['request'] = [
348
				'data' => $requestData,
349
			];
350
		}
351
352
		unset($context['request']['data']['signature']);
353
		unset($context['response']['data']['signature']);
354
		unset($context['response']['data']['extensions']['signature']);
355
356
		$this->logger->info($url, $context);
357
	}
358
359
}
360