Passed
Pull Request — master (#998)
by
unknown
02:16
created

get_alipay_error_response_message()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 9
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 16
rs 9.9666
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yansongda\Pay;
6
7
use JetBrains\PhpStorm\Deprecated;
8
use Psr\Http\Message\ResponseInterface;
9
use Psr\Http\Message\ServerRequestInterface;
10
use Yansongda\Artful\Contract\ConfigInterface;
11
use Yansongda\Artful\Exception\ContainerException;
12
use Yansongda\Artful\Exception\InvalidConfigException;
13
use Yansongda\Artful\Exception\InvalidParamsException;
14
use Yansongda\Artful\Exception\ServiceNotFoundException;
15
use Yansongda\Artful\Plugin\AddPayloadBodyPlugin;
16
use Yansongda\Artful\Plugin\ParserPlugin;
17
use Yansongda\Artful\Plugin\StartPlugin;
18
use Yansongda\Pay\Exception\DecryptException;
19
use Yansongda\Pay\Exception\Exception;
20
use Yansongda\Pay\Exception\InvalidSignException;
21
use Yansongda\Pay\Plugin\Wechat\AddRadarPlugin;
22
use Yansongda\Pay\Plugin\Wechat\ResponsePlugin;
23
use Yansongda\Pay\Plugin\Wechat\V3\AddPayloadSignaturePlugin;
24
use Yansongda\Pay\Plugin\Wechat\V3\WechatPublicCertsPlugin;
25
use Yansongda\Pay\Provider\Alipay;
26
use Yansongda\Pay\Provider\Unipay;
27
use Yansongda\Pay\Provider\Wechat;
28
use Yansongda\Supports\Collection;
29
30
use function Yansongda\Artful\get_radar_body;
31
use function Yansongda\Artful\get_radar_method;
32
33
function get_tenant(array $params = []): string
34
{
35
    return strval($params['_config'] ?? 'default');
36
}
37
38
function get_public_cert(string $key): string
39
{
40
    return is_file($key) ? file_get_contents($key) : $key;
41
}
42
43
function get_private_cert(string $key): string
44
{
45
    if (is_file($key)) {
46
        return file_get_contents($key);
47
    }
48
49
    return "-----BEGIN RSA PRIVATE KEY-----\n".
50
        wordwrap($key, 64, "\n", true).
51
        "\n-----END RSA PRIVATE KEY-----";
52
}
53
54
function get_radar_url(array $config, ?Collection $payload): ?string
55
{
56
    return match ($config['mode'] ?? Pay::MODE_NORMAL) {
57
        Pay::MODE_SERVICE => $payload?->get('_service_url') ?? $payload?->get('_url') ?? null,
58
        Pay::MODE_SANDBOX => $payload?->get('_sandbox_url') ?? $payload?->get('_url') ?? null,
59
        default => $payload?->get('_url') ?? null,
60
    };
61
}
62
63
/**
64
 * @throws ContainerException
65
 * @throws ServiceNotFoundException
66
 */
67
function get_provider_config(string $provider, array $params = []): array
68
{
69
    /** @var ConfigInterface $config */
70
    $config = Pay::get(ConfigInterface::class);
71
72
    return $config->get($provider, [])[get_tenant($params)] ?? [];
73
}
74
75
/**
76
 * @throws ContainerException
77
 * @throws ServiceNotFoundException
78
 */
79
#[Deprecated(reason: '自 v3.7.5 开始废弃', replacement: 'get_provider_config')]
80
function get_alipay_config(array $params = []): array
81
{
82
    $alipay = Pay::get(ConfigInterface::class)->get('alipay');
83
84
    return $alipay[get_tenant($params)] ?? [];
85
}
86
87
function get_alipay_url(array $config, ?Collection $payload): string
88
{
89
    $url = get_radar_url($config, $payload) ?? '';
90
91
    if (str_starts_with($url, 'http')) {
92
        return $url;
93
    }
94
95
    return Alipay::URL[$config['mode'] ?? Pay::MODE_NORMAL];
96
}
97
98
/**
99
 * "{\"alipay_fund_trans_uni_transfer_response\":{\"code\":\"40002\",\"msg\":\"Invalid Arguments\",\"sub_code\":\"isv.invalid-app-id\",\"sub_msg\":\"无效的AppID参数\"}}".
100
 */
101
function get_alipay_error_response_message(array $contents): string
102
{
103
    // 格式化响应内容
104
    foreach (array_keys($contents) as $key) {
105
        if (str_ends_with($key, '_response')) {
106
            $contents = $contents[$key];
107
            break;
108
        }
109
    }
110
111
    $code = $contents['code'] ?? '未知code';
112
    $msg = $contents['msg'] ?? '未知异常';
113
    $subCode = $contents['sub_code'] ?? '未知sub_code';
114
    $subMsg = $contents['sub_msg'] ?? '未知sub_msg';
115
116
    return sprintf('alipay-%s: %s; 错误详情-%s: %s;', $code, $msg, $subCode, $subMsg);
117
}
118
119
/**
120
 * @throws InvalidConfigException
121
 * @throws InvalidSignException
122
 */
123
function verify_alipay_sign(array $config, string $contents, string $sign): void
124
{
125
    if (empty($sign)) {
126
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 验证支付宝签名失败-支付宝签名为空', func_get_args());
127
    }
128
129
    $public = $config['alipay_public_cert_path'] ?? null;
130
131
    if (empty($public)) {
132
        throw new InvalidConfigException(Exception::CONFIG_ALIPAY_INVALID, '配置异常: 缺少支付宝配置 -- [alipay_public_cert_path]');
133
    }
134
135
    $result = 1 === openssl_verify(
136
        $contents,
137
        base64_decode($sign),
138
        get_public_cert($public),
139
        OPENSSL_ALGO_SHA256
140
    );
141
142
    if (!$result) {
143
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证支付宝签名失败', func_get_args());
144
    }
145
}
146
147
/**
148
 * @throws ContainerException
149
 * @throws ServiceNotFoundException
150
 */
151
#[Deprecated(reason: '自 v3.7.5 开始废弃', replacement: 'get_provider_config')]
152
function get_wechat_config(array $params = []): array
153
{
154
    $wechat = Pay::get(ConfigInterface::class)->get('wechat');
155
156
    return $wechat[get_tenant($params)] ?? [];
157
}
158
159
function get_wechat_method(?Collection $payload): string
160
{
161
    return get_radar_method($payload) ?? 'POST';
162
}
163
164
/**
165
 * @throws InvalidParamsException
166
 */
167
function get_wechat_url(array $config, ?Collection $payload): string
168
{
169
    $url = get_radar_url($config, $payload);
170
171
    if (empty($url)) {
172
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_URL_MISSING, '参数异常: 微信 `_url` 或 `_service_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件`');
173
    }
174
175
    if (str_starts_with($url, 'http')) {
176
        return $url;
177
    }
178
179
    return Wechat::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
180
}
181
182
/**
183
 * @throws InvalidParamsException
184
 */
185
function get_wechat_body(?Collection $payload): mixed
186
{
187
    $body = get_radar_body($payload);
188
189
    if (is_null($body)) {
190
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_BODY_MISSING, '参数异常: 微信 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
191
    }
192
193
    return $body;
194
}
195
196
function get_wechat_type_key(array $params): string
197
{
198
    $key = ($params['_type'] ?? 'mp').'_app_id';
199
200
    if ('app_app_id' === $key) {
201
        $key = 'app_id';
202
    }
203
204
    return $key;
205
}
206
207
/**
208
 * @throws InvalidConfigException
209
 */
210
function get_wechat_sign(array $config, string $contents): string
211
{
212
    $privateKey = $config['mch_secret_cert'] ?? null;
213
214
    if (empty($privateKey)) {
215
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_cert]');
216
    }
217
218
    $privateKey = get_private_cert($privateKey);
219
220
    openssl_sign($contents, $sign, $privateKey, 'sha256WithRSAEncryption');
221
222
    return base64_encode($sign);
223
}
224
225
/**
226
 * @throws InvalidConfigException
227
 */
228
function get_wechat_sign_v2(array $config, array $payload, bool $upper = true): string
229
{
230
    $key = $config['mch_secret_key_v2'] ?? null;
231
232
    if (empty($key)) {
233
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
234
    }
235
236
    ksort($payload);
237
238
    $buff = '';
239
240
    foreach ($payload as $k => $v) {
241
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
242
    }
243
244
    $sign = md5($buff.'key='.$key);
245
246
    return $upper ? strtoupper($sign) : $sign;
247
}
248
249
/**
250
 * @throws ContainerException
251
 * @throws DecryptException
252
 * @throws InvalidConfigException
253
 * @throws InvalidParamsException
254
 * @throws InvalidSignException
255
 * @throws ServiceNotFoundException
256
 */
257
function verify_wechat_sign(ResponseInterface|ServerRequestInterface $message, array $params): void
258
{
259
    if ($message instanceof ServerRequestInterface && 'localhost' === $message->getUri()->getHost()) {
260
        return;
261
    }
262
263
    $wechatSerial = $message->getHeaderLine('Wechatpay-Serial');
264
    $timestamp = $message->getHeaderLine('Wechatpay-Timestamp');
265
    $random = $message->getHeaderLine('Wechatpay-Nonce');
266
    $sign = $message->getHeaderLine('Wechatpay-Signature');
267
    $body = (string) $message->getBody();
268
269
    $content = $timestamp."\n".$random."\n".$body."\n";
270
    $public = get_provider_config('wechat', $params)['wechat_public_cert_path'][$wechatSerial] ?? null;
271
272
    if (empty($sign)) {
273
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', ['headers' => $message->getHeaders(), 'body' => $body]);
274
    }
275
276
    $public = get_public_cert(
277
        empty($public) ? reload_wechat_public_certs($params, $wechatSerial) : $public
278
    );
279
280
    $result = 1 === openssl_verify(
281
        $content,
282
        base64_decode($sign),
283
        $public,
284
        'sha256WithRSAEncryption'
285
    );
286
287
    if (!$result) {
288
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', ['headers' => $message->getHeaders(), 'body' => $body]);
289
    }
290
}
291
292
/**
293
 * @throws InvalidConfigException
294
 * @throws InvalidSignException
295
 */
296
function verify_wechat_sign_v2(array $config, array $destination): void
297
{
298
    $sign = $destination['sign'] ?? null;
299
300
    if (empty($sign)) {
301
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 微信签名为空', $destination);
302
    }
303
304
    $key = $config['mch_secret_key_v2'] ?? null;
305
306
    if (empty($key)) {
307
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key_v2]');
308
    }
309
310
    if (get_wechat_sign_v2($config, $destination) !== $sign) {
311
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证微信签名失败', $destination);
312
    }
313
}
314
315
function encrypt_wechat_contents(string $contents, string $publicKey): ?string
316
{
317
    if (openssl_public_encrypt($contents, $encrypted, get_public_cert($publicKey), OPENSSL_PKCS1_OAEP_PADDING)) {
318
        return base64_encode($encrypted);
319
    }
320
321
    return null;
322
}
323
324
function decrypt_wechat_contents(string $encrypted, array $config): ?string
325
{
326
    if (openssl_private_decrypt(base64_decode($encrypted), $decrypted, get_private_cert($config['mch_secret_cert'] ?? ''), OPENSSL_PKCS1_OAEP_PADDING)) {
327
        return $decrypted;
328
    }
329
330
    return null;
331
}
332
333
/**
334
 * @throws ContainerException
335
 * @throws DecryptException
336
 * @throws InvalidConfigException
337
 * @throws InvalidParamsException
338
 * @throws ServiceNotFoundException
339
 */
340
function reload_wechat_public_certs(array $params, ?string $serialNo = null): string
341
{
342
    $data = Pay::wechat()->pay(
343
        [StartPlugin::class, WechatPublicCertsPlugin::class, AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, ResponsePlugin::class, ParserPlugin::class],
344
        $params
345
    )->get('data', []);
0 ignored issues
show
Bug introduced by
The method get() does not exist on Psr\Http\Message\MessageInterface. ( Ignorable by Annotation )

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

345
    )->/** @scrutinizer ignore-call */ get('data', []);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
346
347
    $wechatConfig = get_provider_config('wechat', $params);
348
349
    foreach ($data as $item) {
350
        $certs[$item['serial_no']] = decrypt_wechat_resource($item['encrypt_certificate'], $wechatConfig)['ciphertext'] ?? '';
351
    }
352
353
    Pay::get(ConfigInterface::class)->set(
354
        'wechat.'.get_tenant($params).'.wechat_public_cert_path',
355
        ((array) ($wechatConfig['wechat_public_cert_path'] ?? [])) + ($certs ?? []),
356
    );
357
358
    if (!is_null($serialNo) && empty($certs[$serialNo])) {
359
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 获取微信 wechat_public_cert_path 配置失败');
360
    }
361
362
    return $certs[$serialNo] ?? '';
363
}
364
365
/**
366
 * @throws ContainerException
367
 * @throws DecryptException
368
 * @throws InvalidConfigException
369
 * @throws InvalidParamsException
370
 * @throws ServiceNotFoundException
371
 */
372
function get_wechat_public_certs(array $params = [], ?string $path = null): void
373
{
374
    reload_wechat_public_certs($params);
375
376
    $config = get_provider_config('wechat', $params);
377
378
    if (empty($path)) {
379
        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...
380
381
        return;
382
    }
383
384
    foreach ($config['wechat_public_cert_path'] as $serialNo => $cert) {
385
        file_put_contents($path.'/'.$serialNo.'.crt', $cert);
386
    }
387
}
388
389
/**
390
 * @throws InvalidConfigException
391
 * @throws DecryptException
392
 */
393
function decrypt_wechat_resource(array $resource, array $config): array
394
{
395
    $ciphertext = base64_decode($resource['ciphertext'] ?? '');
396
    $secret = $config['mch_secret_key'] ?? null;
397
398
    if (strlen($ciphertext) <= Wechat::AUTH_TAG_LENGTH_BYTE) {
399
        throw new DecryptException(Exception::DECRYPT_WECHAT_CIPHERTEXT_PARAMS_INVALID, '加解密异常: ciphertext 位数过短');
400
    }
401
402
    if (is_null($secret) || Wechat::MCH_SECRET_KEY_LENGTH_BYTE != strlen($secret)) {
403
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mch_secret_key]');
404
    }
405
406
    $resource['ciphertext'] = match ($resource['algorithm'] ?? '') {
407
        'AEAD_AES_256_GCM' => decrypt_wechat_resource_aes_256_gcm($ciphertext, $secret, $resource['nonce'] ?? '', $resource['associated_data'] ?? ''),
408
        default => throw new DecryptException(Exception::DECRYPT_WECHAT_DECRYPTED_METHOD_INVALID, '加解密异常: algorithm 不支持'),
409
    };
410
411
    return $resource;
412
}
413
414
/**
415
 * @throws DecryptException
416
 */
417
function decrypt_wechat_resource_aes_256_gcm(string $ciphertext, string $secret, string $nonce, string $associatedData): array|string
418
{
419
    $decrypted = openssl_decrypt(
420
        substr($ciphertext, 0, -Wechat::AUTH_TAG_LENGTH_BYTE),
421
        'aes-256-gcm',
422
        $secret,
423
        OPENSSL_RAW_DATA,
424
        $nonce,
425
        substr($ciphertext, -Wechat::AUTH_TAG_LENGTH_BYTE),
426
        $associatedData
427
    );
428
429
    if (false === $decrypted) {
430
        throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 解密失败,请检查微信 mch_secret_key 是否正确');
431
    }
432
433
    if ('certificate' !== $associatedData) {
434
        $decrypted = json_decode($decrypted, true);
435
436
        if (JSON_ERROR_NONE !== json_last_error()) {
437
            throw new DecryptException(Exception::DECRYPT_WECHAT_ENCRYPTED_DATA_INVALID, '加解密异常: 待解密数据非正常数据');
438
        }
439
    }
440
441
    return $decrypted;
442
}
443
444
/**
445
 * @throws ContainerException
446
 * @throws DecryptException
447
 * @throws InvalidConfigException
448
 * @throws InvalidParamsException
449
 * @throws ServiceNotFoundException
450
 */
451
function get_wechat_serial_no(array $params): string
452
{
453
    if (!empty($params['_serial_no'])) {
454
        return $params['_serial_no'];
455
    }
456
457
    $config = get_provider_config('wechat', $params);
458
459
    if (empty($config['wechat_public_cert_path'])) {
460
        reload_wechat_public_certs($params);
461
462
        $config = get_provider_config('wechat', $params);
463
    }
464
465
    mt_srand();
466
467
    return strval(array_rand($config['wechat_public_cert_path']));
468
}
469
470
/**
471
 * @throws InvalidParamsException
472
 */
473
function get_wechat_public_key(array $config, string $serialNo): string
474
{
475
    $publicKey = $config['wechat_public_cert_path'][$serialNo] ?? null;
476
477
    if (empty($publicKey)) {
478
        throw new InvalidParamsException(Exception::PARAMS_WECHAT_SERIAL_NOT_FOUND, '参数异常: 微信公钥序列号为找到 -'.$serialNo);
479
    }
480
481
    return $publicKey;
482
}
483
484
/**
485
 * @throws InvalidConfigException
486
 */
487
function get_wechat_miniprogram_pay_sign(array $config, string $url, string $payload): string
488
{
489
    if (empty($config['mini_app_key_virtual_pay'])) {
490
        throw new InvalidConfigException(Exception::CONFIG_WECHAT_INVALID, '配置异常: 缺少微信配置 -- [mini_app_key_virtual_pay]');
491
    }
492
493
    return hash_hmac('sha256', $url.'&'.$payload, $config['mini_app_key_virtual_pay']);
494
}
495
496
function get_wechat_miniprogram_user_sign(string $sessionKey, string $payload): string
497
{
498
    return hash_hmac('sha256', $payload, $sessionKey);
499
}
500
501
/**
502
 * @throws ContainerException
503
 * @throws ServiceNotFoundException
504
 */
505
#[Deprecated(reason: '自 v3.7.5 开始废弃', replacement: 'get_provider_config')]
506
function get_unipay_config(array $params = []): array
507
{
508
    $unipay = Pay::get(ConfigInterface::class)->get('unipay');
509
510
    return $unipay[get_tenant($params)] ?? [];
511
}
512
513
/**
514
 * @throws InvalidConfigException
515
 * @throws InvalidSignException
516
 */
517
function verify_unipay_sign(array $config, string $contents, string $sign, ?string $signPublicKeyCert = null): void
518
{
519
    if (empty($sign)) {
520
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 银联签名为空', func_get_args());
521
    }
522
523
    if (empty($signPublicKeyCert) && empty($public = $config['unipay_public_cert_path'] ?? null)) {
524
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [unipay_public_cert_path]');
525
    }
526
527
    $result = 1 === openssl_verify(
528
        hash('sha256', $contents),
529
        base64_decode($sign),
530
        get_public_cert($signPublicKeyCert ?? $public ?? ''),
531
        'sha256'
532
    );
533
534
    if (!$result) {
535
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', func_get_args());
536
    }
537
}
538
539
/**
540
 * @throws InvalidParamsException
541
 */
542
function get_unipay_url(array $config, ?Collection $payload): string
543
{
544
    $url = get_radar_url($config, $payload);
545
546
    if (empty($url)) {
547
        throw new InvalidParamsException(Exception::PARAMS_UNIPAY_URL_MISSING, '参数异常: 银联 `_url` 参数缺失:你可能用错插件顺序,应该先使用 `业务插件`');
548
    }
549
550
    if (str_starts_with($url, 'http')) {
551
        return $url;
552
    }
553
554
    return Unipay::URL[$config['mode'] ?? Pay::MODE_NORMAL].$url;
555
}
556
557
/**
558
 * @throws InvalidParamsException
559
 */
560
function get_unipay_body(?Collection $payload): string
561
{
562
    $body = get_radar_body($payload);
563
564
    if (is_null($body)) {
565
        throw new InvalidParamsException(Exception::PARAMS_UNIPAY_BODY_MISSING, '参数异常: 银联 `_body` 参数缺失:你可能用错插件顺序,应该先使用 `AddPayloadBodyPlugin`');
566
    }
567
568
    return $body;
569
}
570
571
/**
572
 * @throws InvalidConfigException
573
 */
574
function get_unipay_sign_qra(array $config, array $payload): string
575
{
576
    $key = $config['mch_secret_key'] ?? null;
577
578
    if (empty($key)) {
579
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [mch_secret_key]');
580
    }
581
582
    ksort($payload);
583
584
    $buff = '';
585
586
    foreach ($payload as $k => $v) {
587
        $buff .= ('sign' != $k && '' != $v && !is_array($v)) ? $k.'='.$v.'&' : '';
588
    }
589
590
    return strtoupper(md5($buff.'key='.$key));
591
}
592
593
/**
594
 * @throws InvalidConfigException
595
 * @throws InvalidSignException
596
 */
597
function verify_unipay_sign_qra(array $config, array $destination): void
598
{
599
    $sign = $destination['sign'] ?? null;
600
601
    if (empty($sign)) {
602
        throw new InvalidSignException(Exception::SIGN_EMPTY, '签名异常: 银联签名为空', $destination);
603
    }
604
605
    $key = $config['mch_secret_key'] ?? null;
606
607
    if (empty($key)) {
608
        throw new InvalidConfigException(Exception::CONFIG_UNIPAY_INVALID, '配置异常: 缺少银联配置 -- [mch_secret_key]');
609
    }
610
611
    if (get_unipay_sign_qra($config, $destination) !== $sign) {
612
        throw new InvalidSignException(Exception::SIGN_ERROR, '签名异常: 验证银联签名失败', $destination);
613
    }
614
}
615