Completed
Pull Request — master (#15)
by
unknown
09:58
created

ApiClient::prepareData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
cc 1
eloc 4
nc 1
nop 2
rs 9.4285
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
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
	/**
50
	 * @param LoggerInterface $logger
51
	 */
52
	public function setLogger(LoggerInterface $logger = null)
53
	{
54
		$this->logger = $logger;
55
	}
56
57
	/**
58
	 * @param string $url
59
	 * @param mixed[]|null $data
60
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
61
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
62
	 * @param \Closure|null $responseValidityCallback
63
	 * @param ResponseExtensionHandler[] $extensions
64
	 * @return Response
65
	 *
66
	 * @throws PrivateKeyFileException
67
	 * @throws SigningFailedException
68
	 * @throws PublicKeyFileException
69
	 * @throws VerificationFailedException
70
	 * @throws RequestException
71
	 * @throws ApiClientDriverException
72
	 * @throws InvalidSignatureException
73
	 */
74 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...
75
		string $url,
76
		array $data = [],
77
		SignatureDataFormatter $requestSignatureDataFormatter,
78
		SignatureDataFormatter $responseSignatureDataFormatter,
79
		\Closure $responseValidityCallback = null,
80
		array $extensions = []
81
	): Response
82
	{
83
		return $this->request(
84
			new HttpMethod(HttpMethod::GET),
85
			$url,
86
			$this->prepareData($data, $requestSignatureDataFormatter),
87
			null,
88
			$responseSignatureDataFormatter,
89
			$responseValidityCallback,
90
			$extensions
91
		);
92
	}
93
94
	/**
95
	 * @param string $url
96
	 * @param mixed[]|null $data
97
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
98
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
99
	 * @param ResponseExtensionHandler[] $extensions
100
	 * @return Response
101
	 *
102
	 * @throws PrivateKeyFileException
103
	 * @throws SigningFailedException
104
	 * @throws PublicKeyFileException
105
	 * @throws VerificationFailedException
106
	 * @throws RequestException
107
	 * @throws ApiClientDriverException
108
	 * @throws InvalidSignatureException
109
	 */
110 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...
111
		string $url,
112
		array $data = [],
113
		SignatureDataFormatter $requestSignatureDataFormatter,
114
		SignatureDataFormatter $responseSignatureDataFormatter,
115
		array $extensions = []
116
	): Response
117
	{
118
		return $this->request(
119
			new HttpMethod(HttpMethod::POST),
120
			$url,
121
			[],
122
			$this->prepareData($data, $requestSignatureDataFormatter),
123
			$responseSignatureDataFormatter,
124
			null,
125
			$extensions
126
		);
127
	}
128
129
	/**
130
	 * @param string $url
131
	 * @param mixed[]|null $data
132
	 * @param SignatureDataFormatter $requestSignatureDataFormatter
133
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
134
	 * @param ResponseExtensionHandler[] $extensions
135
	 * @return Response
136
	 *
137
	 * @throws PrivateKeyFileException
138
	 * @throws SigningFailedException
139
	 * @throws PublicKeyFileException
140
	 * @throws VerificationFailedException
141
	 * @throws RequestException
142
	 * @throws ApiClientDriverException
143
	 * @throws InvalidSignatureException
144
	 */
145 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...
146
		string $url,
147
		array $data = [],
148
		SignatureDataFormatter $requestSignatureDataFormatter,
149
		SignatureDataFormatter $responseSignatureDataFormatter,
150
		array $extensions = []
151
	): Response
152
	{
153
		return $this->request(
154
			new HttpMethod(HttpMethod::PUT),
155
			$url,
156
			[],
157
			$this->prepareData($data, $requestSignatureDataFormatter),
158
			$responseSignatureDataFormatter,
159
			null,
160
			$extensions
161
		);
162
	}
163
164
	/**
165
	 * @param HttpMethod $method
166
	 * @param string $url
167
	 * @param string[] $queries
168
	 * @param mixed[]|null $data
169
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
170
	 * @param \Closure|null $responseValidityCallback
171
	 * @param ResponseExtensionHandler[] $extensions
172
	 * @return Response
173
	 *
174
	 * @throws PrivateKeyFileException
175
	 * @throws SigningFailedException
176
	 * @throws PublicKeyFileException
177
	 * @throws VerificationFailedException
178
	 * @throws RequestException
179
	 * @throws ApiClientDriverException
180
	 * @throws InvalidSignatureException
181
	 */
182
	public function request(
183
		HttpMethod $method,
184
		string $url,
185
		array $queries = [],
186
		array $data = null,
187
		SignatureDataFormatter $responseSignatureDataFormatter,
188
		\Closure $responseValidityCallback = null,
189
		array $extensions = []
190
	): Response
191
	{
192
		foreach ($queries as $key => $value) {
193
			if (strpos($url, '{' . $key . '}') !== false) {
194
				$url = str_replace('{' . $key . '}', urlencode((string) $value), $url);
195
				unset($queries[$key]);
196
			}
197
		}
198
199
		if ($queries !== []) {
200
			throw new \InvalidArgumentException('Arguments are missing URL placeholders: ' . json_encode($queries));
201
		}
202
203
		$response = $this->driver->request(
204
			$method,
205
			$this->apiUrl . '/' . $url,
206
			$data
207
		);
208
209
		$this->logRequest($url, $queries, $data, $response);
210
211
		if ($responseValidityCallback !== null) {
212
			$responseValidityCallback($response);
213
		}
214
215
		if ($response->getResponseCode()->equalsValue(ResponseCode::S200_OK)) {
216
			$decodedExtensions = [];
217
			if ($extensions !== [] && array_key_exists('extensions', $response->getData())) {
218
				foreach ($response->getData()['extensions'] as $extensionData) {
219
					$name = $extensionData['extension'];
220
					if (isset($extensions[$name])) {
221
						$handler = $extensions[$name];
222
						$decodedExtensions[$name] = $handler->createResponse($this->decodeData($extensionData, $handler->getSignatureDataFormatter()));
223
					}
224
				}
225
			}
226
			$responseData = $this->decodeData($response->getData(), $responseSignatureDataFormatter);
227
			unset($responseData['extensions']);
228
229
			return new Response(
230
				$response->getResponseCode(),
231
				$responseData,
232
				$response->getHeaders(),
233
				$decodedExtensions
234
			);
235
236
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S303_SEE_OTHER)) {
237
			return new Response(
238
				$response->getResponseCode(),
239
				null,
240
				$response->getHeaders()
241
			);
242
243
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S400_BAD_REQUEST)) {
244
			throw new BadRequestException($response);
245
246
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S403_FORBIDDEN)) {
247
			throw new ForbiddenException($response);
248
249
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S404_NOT_FOUND)) {
250
			throw new NotFoundException($response);
251
252
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S405_METHOD_NOT_ALLOWED)) {
253
			throw new MethodNotAllowedException($response);
254
255
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S429_TOO_MANY_REQUESTS)) {
256
			throw new TooManyRequestsException($response);
257
258
		} elseif ($response->getResponseCode()->equalsValue(ResponseCode::S503_SERVICE_UNAVAILABLE)) {
259
			throw new ServiceUnavailableException($response);
260
		}
261
262
		throw new InternalErrorException($response);
263
	}
264
265
	/**
266
	 * @param mixed[] $data
267
	 * @param SignatureDataFormatter $responseSignatureDataFormatter
268
	 * @return Response
269
	 *
270
	 * @throws InvalidSignatureException
271
	 * @throws PrivateKeyFileException
272
	 * @throws SigningFailedException
273
	 * @throws PublicKeyFileException
274
	 * @throws VerificationFailedException
275
	 */
276
	public function createResponseByData(array $data, SignatureDataFormatter $responseSignatureDataFormatter): Response
277
	{
278
		$response = new Response(
279
			new ResponseCode(ResponseCode::S200_OK),
280
			$data
281
		);
282
283
		return new Response(
284
			$response->getResponseCode(),
285
			$this->decodeData($data, $responseSignatureDataFormatter),
286
			$response->getHeaders()
287
		);
288
	}
289
290
	/**
291
	 * @param mixed[] $data
292
	 * @param SignatureDataFormatter $signatureDataFormatter
293
	 * @return mixed[]
294
	 *
295
	 * @throws PrivateKeyFileException
296
	 * @throws SigningFailedException
297
	 */
298
	private function prepareData(array $data, SignatureDataFormatter $signatureDataFormatter): array
299
	{
300
		$data['dttm'] = (new DateTimeImmutable())->format('YmdHis');
301
		$data['signature'] = $this->cryptoService->signData($data, $signatureDataFormatter);
302
303
		return $data;
304
	}
305
306
	/**
307
	 * @param mixed[] $responseData
308
	 * @param SignatureDataFormatter $signatureDataFormatter
309
	 * @return mixed[]
310
	 *
311
	 * @throws InvalidSignatureException
312
	 * @throws PublicKeyFileException
313
	 * @throws VerificationFailedException
314
	 */
315
	private function decodeData(array $responseData, SignatureDataFormatter $signatureDataFormatter): array
316
	{
317
		if (!array_key_exists('signature', $responseData)) {
318
			throw new InvalidSignatureException($responseData);
319
		}
320
321
		$signature = $responseData['signature'];
322
		unset($responseData['signature']);
323
324
		if (!$this->cryptoService->verifyData($responseData, $signature, $signatureDataFormatter)) {
325
			throw new InvalidSignatureException($responseData);
326
		}
327
328
		return $responseData;
329
	}
330
331
	private function logRequest(string $url, array $queries, array $requestData = null, Response $response)
332
	{
333
		if ($this->logger === null) {
334
			return;
335
		}
336
337
		$context = [
338
			'type' => 'csobRequest',
339
			'response-code' => $response->getResponseCode()->getValue(),
340
			'response-data' => $response->getData(),
341
		];
342
343
		if (count($queries) !== 0) {
344
			$context['queries'] = $queries;
345
		}
346
347
		if ($requestData !== null) {
348
			$context['request-data'] = $requestData;
349
		}
350
351
		if (count($_GET) !== 0) {
352
			$context['GET'] = $_GET;
353
		}
354
355
		if (count($_GET) !== 0) {
356
			$context['POST'] = $_POST;
357
		}
358
359
		unset($context['request-data']['signature']);
360
		unset($context['response-data']['signature']);
361
362
		$this->logger->info($url, $context);
363
	}
364
365
}
366