Passed
Pull Request — master (#753)
by Songda
01:52
created

from_xml()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
c 0
b 0
f 0
dl 0
loc 13
rs 10
cc 3
nc 3
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yansongda\Pay;
6
7
use const PHP_VERSION_ID;
8
9
use Psr\Http\Message\MessageInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Yansongda\Pay\Contract\ConfigInterface;
12
use Yansongda\Pay\Exception\Exception;
13
use Yansongda\Pay\Exception\InvalidConfigException;
14
use Yansongda\Pay\Exception\InvalidResponseException;
15
use Yansongda\Pay\Parser\NoHttpRequestParser;
16
use Yansongda\Pay\Plugin\ParserPlugin;
17
use Yansongda\Pay\Plugin\Wechat\PreparePlugin;
18
use Yansongda\Pay\Plugin\Wechat\RadarSignPlugin;
19
use Yansongda\Pay\Plugin\Wechat\WechatPublicCertsPlugin;
20
use Yansongda\Pay\Provider\Wechat;
21
use Yansongda\Supports\Str;
22
23
if (!function_exists('should_do_http_request')) {
24
    function should_do_http_request(?string $direction): bool
25
    {
26
        return is_null($direction) ||
27
            (NoHttpRequestParser::class !== $direction &&
28
            !in_array(NoHttpRequestParser::class, class_parents($direction)));
29
    }
30
}
31
32
if (!function_exists('get_tenant')) {
33
    function get_tenant(array $params = []): string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
34
    {
35
        return strval($params['_config'] ?? 'default');
36
    }
37
}
38
39
if (!function_exists('get_alipay_config')) {
40
    /**
41
     * @throws \Yansongda\Pay\Exception\ContainerException
42
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
43
     */
44
    function get_alipay_config(array $params = []): array
45
    {
46
        $alipay = Pay::get(ConfigInterface::class)->get('alipay');
47
48
        return $alipay[get_tenant($params)] ?? [];
49
    }
50
}
51
52
if (!function_exists('get_public_cert')) {
53
    function get_public_cert(string $key): string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
54
    {
55
        return Str::endsWith($key, ['.cer', '.crt', '.pem']) ? file_get_contents($key) : $key;
56
    }
57
}
58
59
if (!function_exists('get_private_cert')) {
60
    function get_private_cert(string $key): string
61
    {
62
        if (Str::endsWith($key, ['.crt', '.pem'])) {
63
            return file_get_contents($key);
64
        }
65
66
        return "-----BEGIN RSA PRIVATE KEY-----\n".
67
            wordwrap($key, 64, "\n", true).
68
            "\n-----END RSA PRIVATE KEY-----";
69
    }
70
}
71
72
if (!function_exists('verify_alipay_sign')) {
73
    /**
74
     * @throws \Yansongda\Pay\Exception\ContainerException
75
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
76
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
77
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
78
     */
79
    function verify_alipay_sign(array $params, string $contents, string $sign): void
80
    {
81
        $public = get_alipay_config($params)['alipay_public_cert_path'] ?? null;
82
83
        if (empty($public)) {
84
            throw new InvalidConfigException(Exception::ALIPAY_CONFIG_ERROR, 'Missing Alipay Config -- [alipay_public_cert_path]');
85
        }
86
87
        $result = 1 === openssl_verify(
88
            $contents,
89
            base64_decode($sign),
90
            get_public_cert($public),
91
            OPENSSL_ALGO_SHA256);
92
93
        if (!$result) {
94
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Alipay Response Sign Failed', func_get_args());
95
        }
96
    }
97
}
98
99
if (!function_exists('get_wechat_config')) {
100
    /**
101
     * @throws \Yansongda\Pay\Exception\ContainerException
102
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
103
     */
104
    function get_wechat_config(array $params): array
105
    {
106
        $wechat = Pay::get(ConfigInterface::class)->get('wechat');
107
108
        return $wechat[get_tenant($params)] ?? [];
109
    }
110
}
111
112
if (!function_exists('get_wechat_base_uri')) {
113
    /**
114
     * @throws \Yansongda\Pay\Exception\ContainerException
115
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
116
     */
117
    function get_wechat_base_uri(array $params): string
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
118
    {
119
        $config = get_wechat_config($params);
120
121
        return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL];
122
    }
123
}
124
125
if (!function_exists('get_wechat_sign')) {
126
    /**
127
     * @throws \Yansongda\Pay\Exception\ContainerException
128
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
129
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
130
     */
131
    function get_wechat_sign(array $params, string $contents): string
132
    {
133
        $privateKey = get_wechat_config($params)['mch_secret_cert'] ?? null;
134
135
        if (empty($privateKey)) {
136
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_cert]');
137
        }
138
139
        $privateKey = get_private_cert($privateKey);
140
141
        openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
142
143
        return base64_encode($sign);
144
    }
145
}
146
147
if (!function_exists('verify_wechat_sign')) {
148
    /**
149
     * @param \Psr\Http\Message\ServerRequestInterface|\Psr\Http\Message\ResponseInterface $message
150
     *
151
     * @throws \Yansongda\Pay\Exception\ContainerException
152
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
153
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
154
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
155
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
156
     */
157
    function verify_wechat_sign(MessageInterface $message, array $params): void
158
    {
159
        if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
160
            return;
161
        }
162
163
        $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
164
        $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
165
        $random = $message->getHeaderLine('Wechatpay-Nonce');
166
        $sign = $message->getHeaderLine('Wechatpay-Signature');
167
        $body = (string) $message->getBody();
168
169
        $content = $timestamp."\n".$random."\n".$body."\n";
170
        $public = get_wechat_config($params)['wechat_public_cert_path'][$wechatSerial] ?? null;
171
172
        if (empty($sign)) {
173
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
174
        }
175
176
        $public = get_public_cert(
177
            empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
178
        );
179
180
        $result = 1 === openssl_verify(
181
            $content,
182
            base64_decode($sign),
183
            $public,
184
            'sha256WithRSAEncryption'
185
        );
186
187
        if (!$result) {
188
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, '', ['headers' => $message->getHeaders(), 'body' => $body]);
189
        }
190
    }
191
}
192
193
if (!function_exists('encrypt_wechat_contents')) {
194
    function encrypt_wechat_contents(string $contents, string $publicKey): ?string
195
    {
196
        if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
197
            return base64_encode($encrypted);
198
        }
199
200
        return null;
201
    }
202
}
203
204
if (!function_exists('reload_wechat_public_certs')) {
205
    /**
206
     * @throws \Yansongda\Pay\Exception\ContainerException
207
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
208
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
209
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
210
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
211
     */
212
    function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
213
    {
214
        $data = Pay::wechat()->pay(
215
            [PreparePlugin::class, WechatPublicCertsPlugin::class, RadarSignPlugin::class, ParserPlugin::class],
216
            $params
217
        )->get('data', []);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Psr\Http\Message\MessageInterface. It seems like you code against a sub-type of Psr\Http\Message\MessageInterface such as Yansongda\Pay\Request. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

217
        )->/** @scrutinizer ignore-call */ get('data', []);
Loading history...
218
219
        foreach ($data as $item) {
220
            $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $params)['ciphertext'] ?? '';
221
        }
222
223
        $wechatConfig = get_wechat_config($params);
224
225
        Pay::get(ConfigInterface::class)->set(
226
            'wechat.'.get_tenant($params).'.wechat_public_cert_path',
227
            ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
228
        );
229
230
        if (!is_null($serialNo) && empty($certs[$serialNo])) {
231
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Get Wechat Public Cert Error');
232
        }
233
234
        return $certs[$serialNo] ?? '';
235
    }
236
}
237
238
if (!function_exists('get_wechat_public_certs')) {
239
    /**
240
     * @throws \Yansongda\Pay\Exception\ContainerException
241
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
242
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
243
     * @throws \Yansongda\Pay\Exception\InvalidParamsException
244
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
245
     */
246
    function get_wechat_public_certs(array $params = [], ?string $path = null): void
247
    {
248
        reload_wechat_public_certs($params);
249
250
        $config = get_wechat_config($params);
251
252
        if (empty($path)) {
253
            var_dump($config['wechat_public_cert_path']);
0 ignored issues
show
Security Debugging Code introduced by
var_dump($config['wechat_public_cert_path']) looks like debug code. Are you sure you do not want to remove it?
Loading history...
254
255
            return;
256
        }
257
258
        foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
259
            file_put_contents($path.'/'.$serialNo.'.crt', $cert);
260
        }
261
    }
262
}
263
264
if (!function_exists('decrypt_wechat_resource')) {
265
    /**
266
     * @throws \Yansongda\Pay\Exception\ContainerException
267
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
268
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
269
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
270
     */
271
    function decrypt_wechat_resource(array $resource, array $params): array
272
    {
273
        $ciphertext = base64_decode($resource['ciphertext'] ?? '');
274
        $secret = get_wechat_config($params)['mch_secret_key'] ?? null;
275
276
        if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
277
            throw new InvalidResponseException(Exception::INVALID_CIPHERTEXT_PARAMS);
278
        }
279
280
        if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
281
            throw new InvalidConfigException(Exception::WECHAT_CONFIG_ERROR, 'Missing Wechat Config -- [mch_secret_key]');
282
        }
283
284
        switch ($resource['algorithm'] ?? '') {
285
            case 'AEAD_AES_256_GCM':
286
                $resource['ciphertext'] = decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? '');
287
                break;
288
            default:
289
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_METHOD);
290
        }
291
292
        return $resource;
293
    }
294
}
295
296
if (!function_exists('decrypt_wechat_resource_aes_256_gcm')) {
297
    /**
298
     * @return array|string
299
     *
300
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
301
     */
302
    function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData)
303
    {
304
        $decrypted = openssl_decrypt(
305
            substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
306
            'aes-256-gcm',
307
            $secret,
308
            OPENSSL_RAW_DATA,
309
            $nonce,
310
            substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
311
            $associatedData
312
        );
313
314
        if ('certificate' !== $associatedData) {
315
            $decrypted = json_decode(strval($decrypted), true);
316
317
            if (JSON_ERROR_NONE !== json_last_error()) {
318
                throw new InvalidResponseException(Exception::INVALID_REQUEST_ENCRYPTED_DATA);
319
            }
320
        }
321
322
        return $decrypted;
323
    }
324
}
325
326
if (!function_exists('get_unipay_config')) {
327
    /**
328
     * @throws \Yansongda\Pay\Exception\ContainerException
329
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
330
     */
331
    function get_unipay_config(array $params): array
332
    {
333
        $unipay = Pay::get(ConfigInterface::class)->get('unipay');
334
335
        return $unipay[get_tenant($params)] ?? [];
336
    }
337
}
338
339
if (!function_exists('verify_unipay_sign')) {
340
    /**
341
     * @throws \Yansongda\Pay\Exception\ContainerException
342
     * @throws \Yansongda\Pay\Exception\InvalidConfigException
343
     * @throws \Yansongda\Pay\Exception\InvalidResponseException
344
     * @throws \Yansongda\Pay\Exception\ServiceNotFoundException
345
     */
346
    function verify_unipay_sign(array $params, string $contents, string $sign): void
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
347
    {
348
        if (empty($params['signPubKeyCert'])
349
            && empty($public = get_unipay_config($params)['unipay_public_cert_path'] ?? null)) {
350
            throw new InvalidConfigException(Exception::UNIPAY_CONFIG_ERROR, 'Missing Unipay Config -- [unipay_public_cert_path]');
351
        }
352
353
        $result = 1 === openssl_verify(
354
            hash('sha256', $contents),
355
            base64_decode($sign),
356
            get_public_cert($params['signPubKeyCert'] ?? $public ?? ''),
357
            'sha256');
358
359
        if (!$result) {
360
            throw new InvalidResponseException(Exception::INVALID_RESPONSE_SIGN, 'Verify Unipay Response Sign Failed', func_get_args());
361
        }
362
    }
363
}
364
365
if (!function_exists('to_xml')) {
366
    function to_xml(array $data, string $root = 'xml'): string
367
    {
368
        $xml = '<'.$root.'>';
369
370
        foreach ($data as $key => $val) {
371
            $xml .= is_numeric($val) ? '<'.$key.'>'.$val.'</'.$key.'>' :
372
                                       '<'.$key.'><![CDATA['.$val.']]></'.$key.'>';
373
        }
374
375
        $xml .= '</'.$root.'>';
376
377
        return $xml;
378
    }
379
}
380
381
if (!function_exists('from_xml')) {
382
    function from_xml(string $xml): array
383
    {
384
        if (empty($xml)) {
385
            return [];
386
        }
387
388
        if (PHP_VERSION_ID < 80000) {
389
            libxml_disable_entity_loader();
390
        }
391
392
        return json_decode(json_encode(
393
            simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE
394
        ), true);
395
    }
396
}
397