Completed
Push — master ( ac42d4...a51e81 )
by Songda
14s queued 12s
created

Support::processingApiResult()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 8.9777
c 0
b 0
f 0
cc 6
nc 4
nop 3
1
<?php
2
3
namespace Yansongda\Pay\Gateways\Alipay;
4
5
use Exception;
6
use Yansongda\Pay\Events;
7
use Yansongda\Pay\Exceptions\GatewayException;
8
use Yansongda\Pay\Exceptions\InvalidArgumentException;
9
use Yansongda\Pay\Exceptions\InvalidConfigException;
10
use Yansongda\Pay\Exceptions\InvalidSignException;
11
use Yansongda\Pay\Gateways\Alipay;
12
use Yansongda\Pay\Log;
13
use Yansongda\Supports\Arr;
14
use Yansongda\Supports\Collection;
15
use Yansongda\Supports\Config;
16
use Yansongda\Supports\Str;
17
use Yansongda\Supports\Traits\HasHttpRequest;
18
19
/**
20
 * @author yansongda <[email protected]>
21
 *
22
 * @property string app_id alipay app_id
23
 * @property string ali_public_key
24
 * @property string private_key
25
 * @property array http http options
26
 * @property string mode current mode
27
 * @property array log log options
28
 * @property string pid ali pid
29
 */
30
class Support
31
{
32
    use HasHttpRequest;
33
34
    /**
35
     * Alipay gateway.
36
     *
37
     * @var string
38
     */
39
    protected $baseUri;
40
41
    /**
42
     * Config.
43
     *
44
     * @var Config
45
     */
46
    protected $config;
47
48
    /**
49
     * Instance.
50
     *
51
     * @var Support
52
     */
53
    private static $instance;
54
55
    /**
56
     * Bootstrap.
57
     *
58
     * @author yansongda <[email protected]>
59
     */
60 View Code Duplication
    private function __construct(Config $config)
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...
61
    {
62
        $this->baseUri = Alipay::URL[$config->get('mode', Alipay::MODE_NORMAL)];
63
        $this->config = $config;
64
65
        $this->setHttpOptions();
66
    }
67
68
    /**
69
     * __get.
70
     *
71
     * @author yansongda <[email protected]>
72
     *
73
     * @param $key
74
     *
75
     * @return mixed|Config|null
76
     */
77
    public function __get($key)
78
    {
79
        return $this->getConfig($key);
80
    }
81
82
    /**
83
     * create.
84
     *
85
     * @author yansongda <[email protected]>
86
     *
87
     * @return Support
88
     */
89 View Code Duplication
    public static function create(Config $config)
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...
90
    {
91
        if ('cli' === php_sapi_name() || !(self::$instance instanceof self)) {
92
            self::$instance = new self($config);
93
        }
94
95
        return self::$instance;
96
    }
97
98
    /**
99
     * getInstance.
100
     *
101
     * @author yansongda <[email protected]>
102
     *
103
     * @throws InvalidArgumentException
104
     *
105
     * @return Support
106
     */
107
    public static function getInstance()
108
    {
109
        if (is_null(self::$instance)) {
110
            throw new InvalidArgumentException('You Should [Create] First Before Using');
111
        }
112
113
        return self::$instance;
114
    }
115
116
    /**
117
     * clear.
118
     *
119
     * @author yansongda <[email protected]>
120
     */
121
    public function clear()
122
    {
123
        self::$instance = null;
124
    }
125
126
    /**
127
     * Get Alipay API result.
128
     *
129
     * @author yansongda <[email protected]>
130
     *
131
     * @throws GatewayException
132
     * @throws InvalidConfigException
133
     * @throws InvalidSignException
134
     */
135
    public static function requestApi(array $data, bool $response = false): Collection
136
    {
137
        Events::dispatch(new Events\ApiRequesting('Alipay', '', self::$instance->getBaseUri(), $data));
138
139
        $data = array_filter($data, function ($value) {
140
            return ('' == $value || is_null($value)) ? false : true;
141
        });
142
143
        $result = json_decode(self::$instance->post('', $data), true);
144
145
        Events::dispatch(new Events\ApiRequested('Alipay', '', self::$instance->getBaseUri(), $result));
146
147
        return self::processingApiResult($data, $result, $response);
148
    }
149
150
    /**
151
     * Generate sign.
152
     *
153
     * @author yansongda <[email protected]>
154
     *
155
     * @throws InvalidConfigException
156
     */
157
    public static function generateSign(array $params): string
158
    {
159
        $privateKey = self::$instance->private_key;
160
161
        if (is_null($privateKey)) {
162
            throw new InvalidConfigException('Missing Alipay Config -- [private_key]');
163
        }
164
165
        if (Str::endsWith($privateKey, '.pem')) {
166
            $privateKey = openssl_pkey_get_private(
167
                Str::startsWith($privateKey, 'file://') ? $privateKey : 'file://'.$privateKey
168
            );
169
        } else {
170
            $privateKey = "-----BEGIN RSA PRIVATE KEY-----\n".
171
                wordwrap($privateKey, 64, "\n", true).
172
                "\n-----END RSA PRIVATE KEY-----";
173
        }
174
175
        openssl_sign(self::getSignContent($params), $sign, $privateKey, OPENSSL_ALGO_SHA256);
176
177
        $sign = base64_encode($sign);
178
179
        Log::debug('Alipay Generate Sign', [$params, $sign]);
180
181
        if (is_resource($privateKey)) {
182
            openssl_free_key($privateKey);
183
        }
184
185
        return $sign;
186
    }
187
188
    /**
189
     * Verify sign.
190
     *
191
     * @author yansongda <[email protected]>
192
     *
193
     * @param bool        $sync
194
     * @param string|null $sign
195
     *
196
     * @throws InvalidConfigException
197
     */
198
    public static function verifySign(array $data, $sync = false, $sign = null): bool
199
    {
200
        $publicKey = self::$instance->ali_public_key;
201
202
        if (is_null($publicKey)) {
203
            throw new InvalidConfigException('Missing Alipay Config -- [ali_public_key]');
204
        }
205
206
        if (Str::endsWith($publicKey, '.crt')) {
207
            $publicKey = file_get_contents($publicKey);
208
        } elseif (Str::endsWith($publicKey, '.pem')) {
209
            $publicKey = openssl_pkey_get_public(
210
                Str::startsWith($publicKey, 'file://') ? $publicKey : 'file://'.$publicKey
211
            );
212
        } else {
213
            $publicKey = "-----BEGIN PUBLIC KEY-----\n".
214
                wordwrap($publicKey, 64, "\n", true).
215
                "\n-----END PUBLIC KEY-----";
216
        }
217
218
        $sign = $sign ?? $data['sign'];
219
220
        $toVerify = $sync ? json_encode($data, JSON_UNESCAPED_UNICODE) : self::getSignContent($data, true);
221
222
        $isVerify = 1 === openssl_verify($toVerify, base64_decode($sign), $publicKey, OPENSSL_ALGO_SHA256);
223
224
        if (is_resource($publicKey)) {
225
            openssl_free_key($publicKey);
226
        }
227
228
        return $isVerify;
229
    }
230
231
    /**
232
     * Get signContent that is to be signed.
233
     *
234
     * @author yansongda <[email protected]>
235
     *
236
     * @param bool $verify
237
     */
238
    public static function getSignContent(array $data, $verify = false): string
239
    {
240
        ksort($data);
241
242
        $stringToBeSigned = '';
243
        foreach ($data as $k => $v) {
244
            if ($verify && 'sign' != $k && 'sign_type' != $k) {
245
                $stringToBeSigned .= $k.'='.$v.'&';
246
            }
247
            if (!$verify && '' !== $v && !is_null($v) && 'sign' != $k && '@' != substr($v, 0, 1)) {
248
                $stringToBeSigned .= $k.'='.$v.'&';
249
            }
250
        }
251
252
        Log::debug('Alipay Generate Sign Content Before Trim', [$data, $stringToBeSigned]);
253
254
        return trim($stringToBeSigned, '&');
255
    }
256
257
    /**
258
     * Convert encoding.
259
     *
260
     * @author yansongda <[email protected]>
261
     *
262
     * @param string|array $data
263
     * @param string       $to
264
     * @param string       $from
265
     */
266
    public static function encoding($data, $to = 'utf-8', $from = 'gb2312'): array
267
    {
268
        return Arr::encoding((array) $data, $to, $from);
269
    }
270
271
    /**
272
     * Get service config.
273
     *
274
     * @author yansongda <[email protected]>
275
     *
276
     * @param string|null $key
277
     * @param mixed|null  $default
278
     *
279
     * @return mixed|null
280
     */
281 View Code Duplication
    public function getConfig($key = null, $default = null)
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...
282
    {
283
        if (is_null($key)) {
284
            return $this->config->all();
285
        }
286
287
        if ($this->config->has($key)) {
288
            return $this->config[$key];
289
        }
290
291
        return $default;
292
    }
293
294
    /**
295
     * Get Base Uri.
296
     *
297
     * @author yansongda <[email protected]>
298
     *
299
     * @return string
300
     */
301
    public function getBaseUri()
302
    {
303
        return $this->baseUri;
304
    }
305
306
    /**
307
     * 生成应用证书SN.
308
     *
309
     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
310
     *
311
     * @param $certPath
312
     *
313
     * @throws /Exception
314
     */
315
    public static function getCertSN($certPath): string
316
    {
317
        if (!is_file($certPath)) {
318
            throw new Exception('unknown certPath -- [getCertSN]');
319
        }
320
        $x509data = file_get_contents($certPath);
321
        if (false === $x509data) {
322
            throw new Exception('Alipay CertSN Error -- [getCertSN]');
323
        }
324
        openssl_x509_read($x509data);
325
        $certdata = openssl_x509_parse($x509data);
326
        if (empty($certdata)) {
327
            throw new Exception('Alipay openssl_x509_parse Error -- [getCertSN]');
328
        }
329
        $issuer_arr = [];
330
        foreach ($certdata['issuer'] as $key => $val) {
331
            $issuer_arr[] = $key.'='.$val;
332
        }
333
        $issuer = implode(',', array_reverse($issuer_arr));
334
        Log::debug('getCertSN:', [$certPath, $issuer, $certdata['serialNumber']]);
335
336
        return md5($issuer.$certdata['serialNumber']);
337
    }
338
339
    /**
340
     * 生成支付宝根证书SN.
341
     *
342
     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
343
     *
344
     * @param $certPath
345
     *
346
     * @return string
347
     *
348
     * @throws /Exception
349
     */
350
    public static function getRootCertSN($certPath)
351
    {
352
        if (!is_file($certPath)) {
353
            throw new Exception('unknown certPath -- [getRootCertSN]');
354
        }
355
        $x509data = file_get_contents($certPath);
356
        if (false === $x509data) {
357
            throw new Exception('Alipay CertSN Error -- [getRootCertSN]');
358
        }
359
        $kCertificateEnd = '-----END CERTIFICATE-----';
360
        $certStrList = explode($kCertificateEnd, $x509data);
361
        $md5_arr = [];
362
        foreach ($certStrList as $one) {
363
            if (!empty(trim($one))) {
364
                $_x509data = $one.$kCertificateEnd;
365
                openssl_x509_read($_x509data);
366
                $_certdata = openssl_x509_parse($_x509data);
367
                if (in_array($_certdata['signatureTypeSN'], ['RSA-SHA256', 'RSA-SHA1'])) {
368
                    $issuer_arr = [];
369
                    foreach ($_certdata['issuer'] as $key => $val) {
370
                        $issuer_arr[] = $key.'='.$val;
371
                    }
372
                    $_issuer = implode(',', array_reverse($issuer_arr));
373
                    if (0 === strpos($_certdata['serialNumber'], '0x')) {
374
                        $serialNumber = self::bchexdec($_certdata['serialNumber']);
375
                    } else {
376
                        $serialNumber = $_certdata['serialNumber'];
377
                    }
378
                    $md5_arr[] = md5($_issuer.$serialNumber);
379
                    Log::debug('getRootCertSN Sub:', [$certPath, $_issuer, $serialNumber]);
380
                }
381
            }
382
        }
383
384
        return implode('_', $md5_arr);
385
    }
386
387
    /**
388
     * processingApiResult.
389
     *
390
     * @author yansongda <[email protected]>
391
     *
392
     * @param $data
393
     * @param $result
394
     *
395
     * @throws GatewayException
396
     * @throws InvalidConfigException
397
     * @throws InvalidSignException
398
     */
399
    protected static function processingApiResult($data, $result, $response = false): Collection
400
    {
401
        if ($response) {
402
            return new Collection($result);
403
        }
404
405
        $method = str_replace('.', '_', $data['method']).'_response';
406
407
        if (!isset($result['sign']) || '10000' != $result[$method]['code']) {
408
            throw new GatewayException('Get Alipay API Error:'.$result[$method]['msg'].(isset($result[$method]['sub_code']) ? (' - '.$result[$method]['sub_code']) : ''), $result);
409
        }
410
411
        if (self::verifySign($result[$method], true, $result['sign'])) {
412
            return new Collection($result[$method]);
413
        }
414
415
        Events::dispatch(new Events\SignFailed('Alipay', '', $result));
416
417
        throw new InvalidSignException('Alipay Sign Verify FAILED', $result);
418
    }
419
420
    /**
421
     * Set Http options.
422
     *
423
     * @author yansongda <[email protected]>
424
     */
425 View Code Duplication
    protected function setHttpOptions(): self
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...
426
    {
427
        if ($this->config->has('http') && is_array($this->config->get('http'))) {
428
            $this->config->forget('http.base_uri');
429
            $this->httpOptions = $this->config->get('http');
430
        }
431
432
        return $this;
433
    }
434
435
    /**
436
     * 0x转高精度数字.
437
     *
438
     * @author 大冰 https://sbing.vip/archives/2019-new-alipay-php-docking.html
439
     *
440
     * @param $hex
441
     *
442
     * @return int|string
443
     */
444
    private static function bchexdec($hex)
445
    {
446
        $dec = 0;
447
        $len = strlen($hex);
448
        for ($i = 1; $i <= $len; ++$i) {
449
            if (ctype_xdigit($hex[$i - 1])) {
450
                $dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
451
            }
452
        }
453
454
        return str_replace('.00', '', $dec);
455
    }
456
}
457