dbXapp 2.0
RAD, CMS, Module und Runtime-IDE fuer dbXapp
Loading...
Searching...
No Matches
dbxShopAmazonPay.class.php
Go to the documentation of this file.
1<?php
2namespace dbx\dbxShop;
3
4use phpseclib3\Crypt\PublicKeyLoader;
5use phpseclib3\Crypt\RSA;
6
8
9 private const ALGORITHM = 'AMZN-PAY-RSASSA-PSS-V2';
10
11 private function config(): array {
12 $cfg = function_exists('dbx') ? dbx()->get_config('dbxShop') : array();
13 $cfg = is_array($cfg) ? $cfg : array();
14 return array(
15 'enabled' => !empty($cfg['payment_amazon_pay_enabled']),
16 'mode' => (string)($cfg['payment_amazon_pay_mode'] ?? 'sandbox') === 'live' ? 'live' : 'sandbox',
17 'region' => in_array((string)($cfg['payment_amazon_pay_region'] ?? 'EU'), array('EU', 'UK', 'US', 'JP'), true) ? (string)$cfg['payment_amazon_pay_region'] : 'EU',
18 'merchant_id' => trim((string)($cfg['payment_amazon_pay_merchant_id'] ?? '')),
19 'store_id' => trim((string)($cfg['payment_amazon_pay_store_id'] ?? '')),
20 'public_key_id' => trim((string)($cfg['payment_amazon_pay_public_key_id'] ?? '')),
21 'private_key' => trim((string)($cfg['payment_amazon_pay_private_key'] ?? '')),
22 'currency' => strtoupper(substr((string)($cfg['default_currency'] ?? 'EUR'), 0, 3)) ?: 'EUR',
23 'sandbox_simulation_code' => trim((string)($cfg['payment_amazon_pay_sandbox_simulation_code'] ?? '')),
24 );
25 }
26
27 public function isConfigured(): bool {
28 $cfg = $this->config();
29 return !empty($cfg['enabled'])
30 && (string)$cfg['merchant_id'] !== ''
31 && (string)$cfg['store_id'] !== ''
32 && (string)$cfg['public_key_id'] !== ''
33 && (string)$cfg['private_key'] !== '';
34 }
35
36 public function mode(): string {
37 return (string)$this->config()['mode'];
38 }
39
40 public function regionCode(): string {
41 $region = (string)$this->config()['region'];
42 if ($region === 'US') return 'na';
43 if ($region === 'JP') return 'jp';
44 return 'eu';
45 }
46
47 private function host(): string {
48 $region = (string)$this->config()['region'];
49 if ($region === 'JP') return 'pay-api.amazon.jp';
50 if ($region === 'EU' || $region === 'UK') return 'pay-api.amazon.eu';
51 return 'pay-api.amazon.com';
52 }
53
54 private function apiBase(): string {
55 $cfg = $this->config();
56 return 'https://' . $this->host() . '/' . (string)$cfg['mode'] . '/v2';
57 }
58
59 private function normalizeHeaderValue(string $value): string {
60 return trim(preg_replace('/\s+/', ' ', $value) ?: $value);
61 }
62
63 private function canonicalUri(string $path): string {
64 $parts = explode('/', $path);
65 foreach ($parts as &$part) {
66 $part = rawurlencode(rawurldecode($part));
67 }
68 unset($part);
69 return implode('/', $parts);
70 }
71
72 private function canonicalQuery(string $query): string {
73 if ($query === '') {
74 return '';
75 }
76 parse_str($query, $params);
77 ksort($params, SORT_STRING);
78 $pairs = array();
79 foreach ($params as $key => $value) {
80 if (is_array($value)) {
81 sort($value, SORT_STRING);
82 foreach ($value as $item) {
83 $pairs[] = rawurlencode((string)$key) . '=' . rawurlencode((string)$item);
84 }
85 } else {
86 $pairs[] = rawurlencode((string)$key) . '=' . rawurlencode((string)$value);
87 }
88 }
89 return implode('&', $pairs);
90 }
91
92 private function authorizationHeader(string $method, string $path, array $headers, string $body): string {
93 $cfg = $this->config();
94 $normalized = array();
95 foreach ($headers as $name => $value) {
96 $normalized[strtolower((string)$name)] = $this->normalizeHeaderValue((string)$value);
97 }
98 ksort($normalized, SORT_STRING);
99
100 $canonicalHeaders = '';
101 foreach ($normalized as $name => $value) {
102 $canonicalHeaders .= $name . ':' . $value . "\n";
103 }
104 $signedHeaders = implode(';', array_keys($normalized));
105 $pathParts = parse_url($path);
106 $canonicalUri = $this->canonicalUri((string)($pathParts['path'] ?? $path));
107 $canonicalQuery = $this->canonicalQuery((string)($pathParts['query'] ?? ''));
108 $canonicalRequest = strtoupper($method) . "\n"
109 . $canonicalUri . "\n"
110 . $canonicalQuery . "\n"
111 . $canonicalHeaders . "\n"
112 . $signedHeaders . "\n"
113 . hash('sha256', $body);
114
115 $stringToSign = self::ALGORITHM . "\n" . hash('sha256', $canonicalRequest);
116 $privateKey = PublicKeyLoader::loadPrivateKey((string)$cfg['private_key'])
117 ->withHash('sha256')
118 ->withMGFHash('sha256')
119 ->withSaltLength(32)
120 ->withPadding(RSA::SIGNATURE_PSS);
121 $signature = base64_encode($privateKey->sign($stringToSign));
122
123 return self::ALGORITHM
124 . ' PublicKeyId=' . (string)$cfg['public_key_id']
125 . ', SignedHeaders=' . $signedHeaders
126 . ', Signature=' . $signature;
127 }
128
129 private function request(string $method, string $path, ?array $payload = null, bool $idempotent = false): array {
130 if (!$this->isConfigured()) {
131 throw new \RuntimeException('Amazon Pay ist nicht vollstaendig konfiguriert.');
132 }
133
134 $cfg = $this->config();
135 $body = $payload !== null ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : '';
136 if (!is_string($body)) {
137 $body = '';
138 }
139
140 $headers = array(
141 'accept' => 'application/json',
142 'content-type' => 'application/json',
143 'x-amz-pay-date' => gmdate('Ymd\THis\Z'),
144 'x-amz-pay-host' => $this->host(),
145 'x-amz-pay-region' => $this->regionCode(),
146 );
147 if ($idempotent) {
148 $headers['x-amz-pay-idempotency-key'] = bin2hex(random_bytes(16));
149 }
150 if ((string)$cfg['mode'] === 'sandbox' && (string)$cfg['sandbox_simulation_code'] !== '') {
151 $headers['x-amz-simulation-code'] = (string)$cfg['sandbox_simulation_code'];
152 }
153 $headers['authorization'] = $this->authorizationHeader($method, '/' . (string)$cfg['mode'] . '/v2' . $path, $headers, $body);
154
155 $httpHeaders = array();
156 foreach ($headers as $name => $value) {
157 $httpHeaders[] = $name . ': ' . $value;
158 }
159
160 $ch = curl_init($this->apiBase() . $path);
161 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
162 curl_setopt($ch, CURLOPT_CUSTOMREQUEST, strtoupper($method));
163 curl_setopt($ch, CURLOPT_TIMEOUT, 25);
164 curl_setopt($ch, CURLOPT_HTTPHEADER, $httpHeaders);
165 if ($body !== '') {
166 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
167 }
168
169 $raw = curl_exec($ch);
170 $status = (int)curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
171 $err = curl_error($ch);
172 curl_close($ch);
173
174 if ($raw === false || $err !== '') {
175 throw new \RuntimeException('Amazon-Pay-Verbindung fehlgeschlagen: ' . $err);
176 }
177
178 $data = json_decode((string)$raw, true);
179 if (!is_array($data)) {
180 $data = array('raw' => (string)$raw);
181 }
182 $data['_http_status'] = $status;
183 if ($status < 200 || $status >= 300) {
184 $msg = $data['message'] ?? $data['reasonCode'] ?? $data['reasonDescription'] ?? ('HTTP ' . $status);
185 throw new \RuntimeException('Amazon-Pay-Fehler: ' . $msg);
186 }
187 return $data;
188 }
189
190 public function createCheckoutSession(array $order, string $returnUrl, string $cancelUrl): array {
191 $cfg = $this->config();
192 $amount = number_format((float)($order['total_gross'] ?? 0), 2, '.', '');
193 $currency = (string)($order['currency'] ?? $cfg['currency'] ?? 'EUR');
194 $payload = array(
195 'webCheckoutDetails' => array(
196 'checkoutReviewReturnUrl' => $returnUrl,
197 'checkoutResultReturnUrl' => $returnUrl,
198 'checkoutCancelUrl' => $cancelUrl,
199 ),
200 'storeId' => (string)$cfg['store_id'],
201 'scopes' => array('name', 'email', 'phoneNumber', 'billingAddress'),
202 'paymentDetails' => array(
203 'paymentIntent' => 'AuthorizeWithCapture',
204 'canHandlePendingAuthorization' => false,
205 'chargeAmount' => array(
206 'amount' => $amount,
207 'currencyCode' => $currency,
208 ),
209 ),
210 'merchantMetadata' => array(
211 'merchantReferenceId' => (string)($order['order_no'] ?? ''),
212 'merchantStoreName' => (string)($cfg['merchant_id'] ?? ''),
213 'noteToBuyer' => 'dbXapp Bestellung ' . (string)($order['order_no'] ?? ''),
214 ),
215 );
216 return $this->request('POST', '/checkoutSessions', $payload, true);
217 }
218
219 public function completeCheckoutSession(string $checkoutSessionId, array $order): array {
220 $amount = number_format((float)($order['total_gross'] ?? 0), 2, '.', '');
221 $currency = (string)($order['currency'] ?? $this->config()['currency'] ?? 'EUR');
222 return $this->request('POST', '/checkoutSessions/' . rawurlencode($checkoutSessionId) . '/complete', array(
223 'chargeAmount' => array(
224 'amount' => $amount,
225 'currencyCode' => $currency,
226 ),
227 ), true);
228 }
229
230 public function redirectUrl(array $checkoutSession): string {
231 $details = is_array($checkoutSession['webCheckoutDetails'] ?? null) ? $checkoutSession['webCheckoutDetails'] : array();
232 foreach (array('amazonPayRedirectUrl', 'checkoutUrl') as $key) {
233 if (!empty($details[$key])) {
234 return (string)$details[$key];
235 }
236 }
237 return '';
238 }
239
240 public function testConnection(): array {
241 if (!$this->isConfigured()) {
242 return array(
243 'ok' => false,
244 'mode' => $this->mode(),
245 'region' => $this->regionCode(),
246 'message' => 'Amazon Pay ist nicht vollstaendig konfiguriert.',
247 );
248 }
249
250 try {
251 $this->authorizationHeader('GET', '/' . $this->mode() . '/v2/checkoutSessions/test', array(
252 'accept' => 'application/json',
253 'content-type' => 'application/json',
254 'x-amz-pay-date' => gmdate('Ymd\THis\Z'),
255 'x-amz-pay-host' => $this->host(),
256 'x-amz-pay-region' => $this->regionCode(),
257 ), '');
258 return array(
259 'ok' => true,
260 'mode' => $this->mode(),
261 'region' => $this->regionCode(),
262 'message' => 'Amazon-Pay-Konfiguration und RSA-PSS-Signatur sind lokal gueltig. Der Live-API-Test erfolgt bei einer echten Checkout Session.',
263 );
264 } catch (\Throwable $e) {
265 return array(
266 'ok' => false,
267 'mode' => $this->mode(),
268 'region' => $this->regionCode(),
269 'message' => $e->getMessage(),
270 );
271 }
272 }
273}
274?>
$cfg
completeCheckoutSession(string $checkoutSessionId, array $order)
createCheckoutSession(array $order, string $returnUrl, string $cancelUrl)
if( $syncRequest)
Definition index.php:520
DBX schema administration.