提交的内容
This commit is contained in:
27
vendor/w7corp/easywechat/src/Pay/Application.php
vendored
Executable file → Normal file
27
vendor/w7corp/easywechat/src/Pay/Application.php
vendored
Executable file → Normal file
@ -11,6 +11,8 @@ use EasyWeChat\Kernel\Support\PublicKey;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithConfig;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithHttpClient;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithServerRequest;
|
||||
use EasyWeChat\Pay\Contracts\Validator as ValidatorInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class Application implements \EasyWeChat\Pay\Contracts\Application
|
||||
@ -18,9 +20,12 @@ class Application implements \EasyWeChat\Pay\Contracts\Application
|
||||
use InteractWithConfig;
|
||||
use InteractWithHttpClient;
|
||||
use InteractWithServerRequest;
|
||||
use LoggerAwareTrait;
|
||||
|
||||
protected ?ServerInterface $server = null;
|
||||
|
||||
protected ?ValidatorInterface $validator = null;
|
||||
|
||||
protected ?HttpClientInterface $client = null;
|
||||
|
||||
protected ?Merchant $merchant = null;
|
||||
@ -54,6 +59,26 @@ class Application implements \EasyWeChat\Pay\Contracts\Application
|
||||
return $this->merchant;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getValidator(): ValidatorInterface
|
||||
{
|
||||
if (! $this->validator) {
|
||||
$this->validator = new Validator($this->getMerchant());
|
||||
}
|
||||
|
||||
return $this->validator;
|
||||
}
|
||||
|
||||
public function setValidator(ValidatorInterface $validator): static
|
||||
{
|
||||
$this->validator = $validator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \ReflectionException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
@ -94,7 +119,7 @@ class Application implements \EasyWeChat\Pay\Contracts\Application
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
*/
|
||||
public function getClient(): HttpClientInterface
|
||||
public function getClient(): Client|HttpClientInterface
|
||||
{
|
||||
return $this->client ?? $this->client = (new Client(
|
||||
$this->getMerchant(),
|
||||
|
||||
133
vendor/w7corp/easywechat/src/Pay/Client.php
vendored
Executable file → Normal file
133
vendor/w7corp/easywechat/src/Pay/Client.php
vendored
Executable file → Normal file
@ -6,6 +6,8 @@ namespace EasyWeChat\Pay;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Form\File;
|
||||
use EasyWeChat\Kernel\Form\Form;
|
||||
use EasyWeChat\Kernel\HttpClient\HttpClientMethods;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestUtil;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestWithPresets;
|
||||
@ -16,20 +18,25 @@ use EasyWeChat\Kernel\Support\UserAgent;
|
||||
use EasyWeChat\Kernel\Support\Xml;
|
||||
use EasyWeChat\Kernel\Traits\MockableHttpClient;
|
||||
use Exception;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use Mockery;
|
||||
use Mockery\Mock;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use function str_starts_with;
|
||||
use Symfony\Component\HttpClient\DecoratorTrait;
|
||||
use Symfony\Component\HttpClient\HttpClient as SymfonyHttpClient;
|
||||
use Symfony\Component\HttpClient\HttpClientTrait;
|
||||
use Symfony\Component\HttpClient\MockHttpClient;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function ltrim;
|
||||
use function str_starts_with;
|
||||
use function strcasecmp;
|
||||
|
||||
/**
|
||||
* @method ResponseInterface get(string $uri, array $options = [])
|
||||
* @method ResponseInterface post(string $uri, array $options = [])
|
||||
@ -44,13 +51,13 @@ class Client implements HttpClientInterface
|
||||
use DecoratorTrait {
|
||||
DecoratorTrait::withOptions insteadof HttpClientTrait;
|
||||
}
|
||||
use HttpClientTrait;
|
||||
use HttpClientMethods;
|
||||
use HttpClientTrait;
|
||||
use MockableHttpClient;
|
||||
use RequestWithPresets;
|
||||
|
||||
/**
|
||||
* @var array<string, mixed>
|
||||
* @var array{base_uri:string,headers:array{'Content-Type':string,Accept:string}}
|
||||
*/
|
||||
protected array $defaultOptions = [
|
||||
'base_uri' => 'https://api.mch.weixin.qq.com/',
|
||||
@ -60,13 +67,23 @@ class Client implements HttpClientInterface
|
||||
],
|
||||
];
|
||||
|
||||
public const V3_URI_PREFIXES = [
|
||||
protected const V3_URI_PREFIXES = [
|
||||
'/v3/',
|
||||
'/sandbox/v3/',
|
||||
'/hk/v3/',
|
||||
'/global/v3/',
|
||||
];
|
||||
|
||||
/**
|
||||
* Special absolute path string over `GET` method
|
||||
*/
|
||||
protected const V2_URI_OVER_GETS = [
|
||||
'/appauth/getaccesstoken', // secret API which's respond `JSON`, must keep in the first
|
||||
'/papay/entrustweb',
|
||||
'/papay/h5entrustweb',
|
||||
'/papay/partner/entrustweb',
|
||||
'/papay/partner/h5entrustweb',
|
||||
];
|
||||
|
||||
protected bool $throw = true;
|
||||
|
||||
/**
|
||||
@ -82,7 +99,7 @@ class Client implements HttpClientInterface
|
||||
$this->defaultOptions = array_merge(self::OPTIONS_DEFAULTS, $this->defaultOptions);
|
||||
|
||||
if (! empty($defaultOptions)) {
|
||||
$defaultOptions = RequestUtil::formatDefaultOptions($this->defaultOptions);
|
||||
$defaultOptions = RequestUtil::formatDefaultOptions($defaultOptions);
|
||||
[, $this->defaultOptions] = self::prepareRequest(null, null, $defaultOptions, $this->defaultOptions);
|
||||
}
|
||||
|
||||
@ -101,15 +118,19 @@ class Client implements HttpClientInterface
|
||||
$options['headers'] = [];
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$options = $this->mergeThenResetPrepends($options);
|
||||
|
||||
$options['headers']['User-Agent'] = UserAgent::create();
|
||||
|
||||
if ($this->isV3Request($url)) {
|
||||
[, $options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
|
||||
$options['headers']['Authorization'] = $this->createSignature($method, $url, $options);
|
||||
[, $_options] = $this->prepareRequest($method, $url, $options, $this->defaultOptions, true);
|
||||
|
||||
// 部分签名算法需要使用到 body 中额外的部分,所以交由前置逻辑自行完成
|
||||
if (empty($options['headers']['Authorization'])) {
|
||||
$options['headers']['Authorization'] = $this->createSignature($method, $url, $_options);
|
||||
}
|
||||
} else {
|
||||
// v2 全部为 xml 请求
|
||||
if (! empty($options['xml'])) {
|
||||
if (! strcasecmp($method, 'POST') && ! empty($options['xml'])) {
|
||||
if (is_array($options['xml'])) {
|
||||
$options['xml'] = Xml::build($this->attachLegacySignature($options['xml']));
|
||||
}
|
||||
@ -126,26 +147,47 @@ class Client implements HttpClientInterface
|
||||
$options['body'] = Xml::build($this->attachLegacySignature($options['body']));
|
||||
}
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
if (! strcasecmp($method, 'GET') && in_array($url, self::V2_URI_OVER_GETS) && is_array($options['query'] ?? null)) {
|
||||
$options['query'] = $this->attachLegacySignature($options['query']);
|
||||
}
|
||||
|
||||
if (! isset($options['headers']['Content-Type']) && ! isset($options['headers']['content-type'])) {
|
||||
$options['headers']['Content-Type'] = 'text/xml'; /** @phpstan-ignore-line */
|
||||
$options['headers']['Content-Type'] = 'text/xml';
|
||||
}
|
||||
}
|
||||
|
||||
// 合并通过 withHeader 和 withHeaders 设置的信息
|
||||
if (! empty($this->prependHeaders)) {
|
||||
$options['headers'] = array_merge($this->prependHeaders, $options['headers'] ?? []);
|
||||
$options['headers'] = array_merge($this->prependHeaders, $options['headers']);
|
||||
}
|
||||
|
||||
return new Response($this->client->request($method, $url, $options), throw: $this->throw);
|
||||
return new Response(
|
||||
$this->client->request($method, $url, $options),
|
||||
failureJudge: $this->isV3Request($url) ? null : function (Response $response) use ($url): bool {
|
||||
$arr = $response->toArray();
|
||||
|
||||
if ($url === self::V2_URI_OVER_GETS[0]) {
|
||||
return ! (array_key_exists('retcode', $arr) && $arr['retcode'] === 0);
|
||||
}
|
||||
|
||||
return ! (
|
||||
// protocol code, most similar to the HTTP status code in APIv3
|
||||
array_key_exists('return_code', $arr) && $arr['return_code'] === 'SUCCESS'
|
||||
) || (
|
||||
// business code, most similar to the Response.JSON.code in APIv3
|
||||
array_key_exists('result_code', $arr) && $arr['result_code'] !== 'SUCCESS'
|
||||
);
|
||||
},
|
||||
throw: $this->throw
|
||||
);
|
||||
}
|
||||
|
||||
protected function isV3Request(string $url): bool
|
||||
{
|
||||
$uri = new Uri($url);
|
||||
$uri = '/'.ltrim((new Uri($url))->getPath(), '/');
|
||||
|
||||
foreach (self::V3_URI_PREFIXES as $prefix) {
|
||||
if (str_starts_with('/'.ltrim($uri->getPath(), '/'), $prefix)) {
|
||||
if (str_starts_with($uri, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -153,8 +195,23 @@ class Client implements HttpClientInterface
|
||||
return false;
|
||||
}
|
||||
|
||||
public function withSerialHeader(?string $serial = null): static
|
||||
{
|
||||
$platformCerts = $this->merchant->getPlatformCerts();
|
||||
if (empty($platformCerts)) {
|
||||
throw new InvalidConfigException('Missing platform certificate.');
|
||||
}
|
||||
|
||||
$serial ??= array_key_first($platformCerts);
|
||||
$this->withHeader('Wechatpay-Serial', $serial);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $arguments
|
||||
*
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
*/
|
||||
public function __call(string $name, array $arguments): mixed
|
||||
{
|
||||
@ -165,6 +222,33 @@ class Client implements HttpClientInterface
|
||||
return $this->client->$name(...$arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
|
||||
*/
|
||||
public function uploadMedia(string $uri, string $pathOrContents, ?array $meta = null, ?string $filename = null): ResponseInterface
|
||||
{
|
||||
$isFile = is_file($pathOrContents);
|
||||
|
||||
$meta = self::jsonEncode($meta ?? [
|
||||
'filename' => $isFile ? basename($pathOrContents) : $filename ?? 'file',
|
||||
'sha256' => $isFile ? hash_file('sha256', $pathOrContents) : hash('sha256', $pathOrContents),
|
||||
]);
|
||||
|
||||
$form = Form::create([
|
||||
'file' => File::from($pathOrContents),
|
||||
'meta' => new DataPart($meta, null, 'application/json'),
|
||||
]);
|
||||
|
||||
$options = $signatureOptions = $form->toOptions();
|
||||
|
||||
$signatureOptions['body'] = $meta;
|
||||
|
||||
$options['headers']['Authorization'] = $this->createSignature('POST', $uri, $signatureOptions);
|
||||
|
||||
return $this->request('POST', $uri, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $options
|
||||
*
|
||||
@ -200,6 +284,17 @@ class Client implements HttpClientInterface
|
||||
Mockery::mock(PublicKey::class),
|
||||
'mock-v3-key',
|
||||
'mock-v2-key',
|
||||
[
|
||||
'PUB_KEY_ID_MOCK' => '-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlReZ1YnfAohRIfUqIeyP
|
||||
aO0PlkMw1RLPdZbEZmldbGrIrOh/0XqSzNZ+mtB6H0eB7TSaoGFtdp/AWy3tb67m
|
||||
1T62OrEhz6bnSKMcZkYVmODyxZvcwsCZ3zqCaFo7FrGmh1o9M0/Xfa5SOX4jVGni
|
||||
3iM7r7YD/NiW2RCYDtjMoLTmVgrzv45Mzu2XpJqtNbUJIRRhVSnjsAZRC6spWH+b
|
||||
QpYIkVd4qmYE0qdpIQBMYOV1w7v1pYn6Z5QdKG4keemADTn4QaZZHrryTcHNYVsZ
|
||||
2OZ3aybrevSV3wDGnYGk2nt2xtkdfaNfFn4dGW+p4an5M4fRK+CnYpeTgI6POABk
|
||||
pwIDAQAB
|
||||
-----END PUBLIC KEY-----',
|
||||
]
|
||||
);
|
||||
|
||||
return Mockery::mock(static::class, [$mockMerchant, $mockHttpClient])
|
||||
|
||||
0
vendor/w7corp/easywechat/src/Pay/Config.php
vendored
Executable file → Normal file
0
vendor/w7corp/easywechat/src/Pay/Config.php
vendored
Executable file → Normal file
0
vendor/w7corp/easywechat/src/Pay/Contracts/Application.php
vendored
Executable file → Normal file
0
vendor/w7corp/easywechat/src/Pay/Contracts/Application.php
vendored
Executable file → Normal file
5
vendor/w7corp/easywechat/src/Pay/Contracts/Merchant.php
vendored
Executable file → Normal file
5
vendor/w7corp/easywechat/src/Pay/Contracts/Merchant.php
vendored
Executable file → Normal file
@ -20,4 +20,9 @@ interface Merchant
|
||||
public function getCertificate(): PublicKey;
|
||||
|
||||
public function getPlatformCert(string $serial): ?PublicKey;
|
||||
|
||||
/**
|
||||
* @return array<string,PublicKey>
|
||||
*/
|
||||
public function getPlatformCerts(): array;
|
||||
}
|
||||
|
||||
7
vendor/w7corp/easywechat/src/Pay/Contracts/ResponseValidator.php
vendored
Executable file → Normal file
7
vendor/w7corp/easywechat/src/Pay/Contracts/ResponseValidator.php
vendored
Executable file → Normal file
@ -4,13 +4,10 @@ declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay\Contracts;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\BadResponseException;
|
||||
use EasyWeChat\Kernel\HttpClient\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
interface ResponseValidator
|
||||
{
|
||||
/**
|
||||
* @throws BadResponseException if the response is not successful.
|
||||
*/
|
||||
public function validate(ResponseInterface $response): void;
|
||||
public function validate(ResponseInterface|Response $response): void;
|
||||
}
|
||||
|
||||
15
vendor/w7corp/easywechat/src/Pay/Contracts/Validator.php
vendored
Normal file
15
vendor/w7corp/easywechat/src/Pay/Contracts/Validator.php
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay\Contracts;
|
||||
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
|
||||
interface Validator
|
||||
{
|
||||
/**
|
||||
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException if signature validate failed.
|
||||
*/
|
||||
public function validate(MessageInterface $message): void;
|
||||
}
|
||||
9
vendor/w7corp/easywechat/src/Pay/Exceptions/EncryptionFailureException.php
vendored
Normal file
9
vendor/w7corp/easywechat/src/Pay/Exceptions/EncryptionFailureException.php
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace EasyWeChat\Pay\Exceptions;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
|
||||
class EncryptionFailureException extends RuntimeException
|
||||
{
|
||||
}
|
||||
9
vendor/w7corp/easywechat/src/Pay/Exceptions/InvalidSignatureException.php
vendored
Normal file
9
vendor/w7corp/easywechat/src/Pay/Exceptions/InvalidSignatureException.php
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace EasyWeChat\Pay\Exceptions;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
|
||||
class InvalidSignatureException extends RuntimeException
|
||||
{
|
||||
}
|
||||
9
vendor/w7corp/easywechat/src/Pay/LegacySignature.php
vendored
Executable file → Normal file
9
vendor/w7corp/easywechat/src/Pay/LegacySignature.php
vendored
Executable file → Normal file
@ -4,11 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function call_user_func_array;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
|
||||
use function call_user_func_array;
|
||||
use function hash_hmac;
|
||||
use function http_build_query;
|
||||
use function is_string;
|
||||
@ -39,7 +40,9 @@ class LegacySignature
|
||||
'sub_appid' => $params['sub_appid'] ?? null,
|
||||
],
|
||||
$params
|
||||
)
|
||||
),
|
||||
static fn ($value, $key) => ! ($key === 'sign' || $value === '' || is_null($value)),
|
||||
ARRAY_FILTER_USE_BOTH
|
||||
);
|
||||
|
||||
ksort($attributes);
|
||||
@ -50,7 +53,7 @@ class LegacySignature
|
||||
throw new InvalidConfigException('Missing V2 API key.');
|
||||
}
|
||||
|
||||
if (! empty($params['sign_type']) && 'HMAC-SHA256' === $params['sign_type']) {
|
||||
if (! empty($params['sign_type']) && $params['sign_type'] === 'HMAC-SHA256') {
|
||||
$signType = fn (string $message): string => hash_hmac('sha256', $message, $attributes['key']);
|
||||
} else {
|
||||
$signType = 'md5';
|
||||
|
||||
10
vendor/w7corp/easywechat/src/Pay/Merchant.php
vendored
Executable file → Normal file
10
vendor/w7corp/easywechat/src/Pay/Merchant.php
vendored
Executable file → Normal file
@ -4,12 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function array_is_list;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Support\PrivateKey;
|
||||
use EasyWeChat\Kernel\Support\PublicKey;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
|
||||
use function array_is_list;
|
||||
use function intval;
|
||||
use function is_string;
|
||||
|
||||
@ -67,8 +68,13 @@ class Merchant implements MerchantInterface
|
||||
return $this->platformCerts[$serial] ?? null;
|
||||
}
|
||||
|
||||
public function getPlatformCerts(): array
|
||||
{
|
||||
return $this->platformCerts;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key, string|PublicKey> $platformCerts
|
||||
* @param array<array-key, mixed> $platformCerts
|
||||
* @return array<string, PublicKey>
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
|
||||
3
vendor/w7corp/easywechat/src/Pay/Message.php
vendored
Executable file → Normal file
3
vendor/w7corp/easywechat/src/Pay/Message.php
vendored
Executable file → Normal file
@ -2,10 +2,11 @@
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @property string $trade_state
|
||||
|
||||
62
vendor/w7corp/easywechat/src/Pay/ResponseValidator.php
vendored
Executable file → Normal file
62
vendor/w7corp/easywechat/src/Pay/ResponseValidator.php
vendored
Executable file → Normal file
@ -4,26 +4,13 @@ declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_decode;
|
||||
use EasyWeChat\Kernel\Exceptions\BadResponseException;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\HttpClient\Response as HttpClientResponse;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use const OPENSSL_ALGO_SHA256;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use function strval;
|
||||
use Psr\Http\Message\ResponseInterface as PsrResponse;
|
||||
|
||||
class ResponseValidator implements \EasyWeChat\Pay\Contracts\ResponseValidator
|
||||
{
|
||||
public const MAX_ALLOWED_CLOCK_OFFSET = 300;
|
||||
|
||||
public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';
|
||||
|
||||
public const HEADER_NONCE = 'Wechatpay-Nonce';
|
||||
|
||||
public const HEADER_SERIAL = 'Wechatpay-Serial';
|
||||
|
||||
public const HEADER_SIGNATURE = 'Wechatpay-Signature';
|
||||
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
@ -31,48 +18,19 @@ class ResponseValidator implements \EasyWeChat\Pay\Contracts\ResponseValidator
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\BadResponseException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
*/
|
||||
public function validate(ResponseInterface $response): void
|
||||
public function validate(PsrResponse|HttpClientResponse $response): void
|
||||
{
|
||||
if ($response instanceof HttpClientResponse) {
|
||||
$response = $response->toPsrResponse();
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new BadResponseException('Request Failed');
|
||||
}
|
||||
|
||||
foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
|
||||
if (! $response->hasHeader($header)) {
|
||||
throw new BadResponseException("Missing Header: {$header}");
|
||||
}
|
||||
}
|
||||
|
||||
[$timestamp] = $response->getHeader(self::HEADER_TIMESTAMP);
|
||||
[$nonce] = $response->getHeader(self::HEADER_NONCE);
|
||||
[$serial] = $response->getHeader(self::HEADER_SERIAL);
|
||||
[$signature] = $response->getHeader(self::HEADER_SIGNATURE);
|
||||
|
||||
$body = (string) $response->getBody();
|
||||
|
||||
$message = "{$timestamp}\n{$nonce}\n{$body}\n";
|
||||
|
||||
if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
|
||||
throw new BadResponseException('Clock Offset Exceeded');
|
||||
}
|
||||
|
||||
$publicKey = $this->merchant->getPlatformCert($serial);
|
||||
|
||||
if (! $publicKey) {
|
||||
throw new InvalidConfigException(
|
||||
"No platform certs found for serial: {$serial},
|
||||
please download from wechat pay and set it in merchant config with key `certs`."
|
||||
);
|
||||
}
|
||||
|
||||
if (false === \openssl_verify(
|
||||
$message,
|
||||
base64_decode($signature),
|
||||
strval($publicKey),
|
||||
OPENSSL_ALGO_SHA256
|
||||
)) {
|
||||
throw new BadResponseException('Invalid Signature');
|
||||
}
|
||||
(new Validator($this->merchant))->validate($response);
|
||||
}
|
||||
}
|
||||
|
||||
89
vendor/w7corp/easywechat/src/Pay/Server.php
vendored
Executable file → Normal file
89
vendor/w7corp/easywechat/src/Pay/Server.php
vendored
Executable file → Normal file
@ -6,20 +6,25 @@ use Closure;
|
||||
use EasyWeChat\Kernel\Contracts\Server as ServerInterface;
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
|
||||
use EasyWeChat\Kernel\Exceptions\RuntimeException;
|
||||
use EasyWeChat\Kernel\HttpClient\RequestUtil;
|
||||
use EasyWeChat\Kernel\ServerResponse;
|
||||
use EasyWeChat\Kernel\Support\AesEcb;
|
||||
use EasyWeChat\Kernel\Support\AesGcm;
|
||||
use EasyWeChat\Kernel\Support\Xml;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithHandlers;
|
||||
use EasyWeChat\Kernel\Traits\InteractWithServerRequest;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function is_array;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use Nyholm\Psr7\Response;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
use function array_key_exists;
|
||||
use function is_array;
|
||||
use function is_string;
|
||||
use function json_decode;
|
||||
use function json_encode;
|
||||
use function str_contains;
|
||||
use function strval;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @link https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
|
||||
@ -28,17 +33,13 @@ use Throwable;
|
||||
class Server implements ServerInterface
|
||||
{
|
||||
use InteractWithHandlers;
|
||||
use InteractWithServerRequest;
|
||||
|
||||
protected ServerRequestInterface $request;
|
||||
|
||||
/**
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(
|
||||
protected MerchantInterface $merchant,
|
||||
?ServerRequestInterface $request,
|
||||
) {
|
||||
$this->request = $request ?? RequestUtil::createDefaultServerRequest();
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -110,14 +111,72 @@ class Server implements ServerInterface
|
||||
*/
|
||||
public function getRequestMessage(?ServerRequestInterface $request = null): \EasyWeChat\Kernel\Message|Message
|
||||
{
|
||||
$originContent = (string) ($request ?? $this->request)->getBody();
|
||||
$attributes = json_decode($originContent, true);
|
||||
$originContent = (string) ($request ?? $this->getRequest())->getBody();
|
||||
|
||||
// 微信支付的回调数据回调,偶尔是 XML https://github.com/w7corp/easywechat/issues/2737
|
||||
$contentType = ($request ?? $this->getRequest())->getHeaderLine('content-type');
|
||||
$isXml = (str_contains($contentType, 'text/xml') || str_contains($contentType, 'application/xml')) && str_starts_with($originContent, '<xml');
|
||||
$attributes = $isXml ? $this->decodeXmlMessage($originContent) : $this->decodeJsonMessage($originContent);
|
||||
|
||||
return new Message($attributes, $originContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
|
||||
*/
|
||||
protected function decodeXmlMessage(string $contents): array
|
||||
{
|
||||
$attributes = Xml::parse($contents);
|
||||
|
||||
if (! is_array($attributes)) {
|
||||
throw new RuntimeException('Invalid request body.');
|
||||
}
|
||||
|
||||
if (empty($attributes['resource']['ciphertext'])) {
|
||||
if (! empty($attributes['req_info'])) {
|
||||
$key = $this->merchant->getV2SecretKey();
|
||||
|
||||
if (empty($key)) {
|
||||
throw new InvalidArgumentException('V2 secret key is required.');
|
||||
}
|
||||
|
||||
$attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));
|
||||
}
|
||||
|
||||
if (
|
||||
is_array($attributes)
|
||||
&& array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])
|
||||
&& array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])
|
||||
&& array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])
|
||||
) {
|
||||
$attributes += Xml::parse(AesGcm::decrypt(
|
||||
$attributes['event_ciphertext'],
|
||||
$this->merchant->getSecretKey(),
|
||||
$attributes['event_nonce'],
|
||||
$attributes['event_associated_data'] // maybe empty string
|
||||
));
|
||||
}
|
||||
|
||||
if (! is_array($attributes)) {
|
||||
throw new RuntimeException('Failed to decrypt request message.');
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
|
||||
*/
|
||||
protected function decodeJsonMessage(string $contents): array
|
||||
{
|
||||
$attributes = json_decode($contents, true);
|
||||
|
||||
if (! (is_array($attributes) && is_array($attributes['resource']))) {
|
||||
throw new RuntimeException('Invalid request body.');
|
||||
}
|
||||
|
||||
if (empty($attributes['resource']['ciphertext'] ?? null)) {
|
||||
throw new RuntimeException('Invalid request.');
|
||||
}
|
||||
|
||||
@ -135,7 +194,7 @@ class Server implements ServerInterface
|
||||
throw new RuntimeException('Failed to decrypt request message.');
|
||||
}
|
||||
|
||||
return new Message($attributes, $originContent);
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
14
vendor/w7corp/easywechat/src/Pay/Signature.php
vendored
Executable file → Normal file
14
vendor/w7corp/easywechat/src/Pay/Signature.php
vendored
Executable file → Normal file
@ -4,13 +4,17 @@ declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_encode;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
use function http_build_query;
|
||||
use function ltrim;
|
||||
use Nyholm\Psr7\Uri;
|
||||
use Stringable;
|
||||
|
||||
use function array_merge;
|
||||
use function base64_encode;
|
||||
use function http_build_query;
|
||||
use function is_scalar;
|
||||
use function ltrim;
|
||||
use function openssl_sign;
|
||||
use function parse_str;
|
||||
use function strtoupper;
|
||||
@ -33,7 +37,7 @@ class Signature
|
||||
$uri = new Uri($url);
|
||||
|
||||
parse_str($uri->getQuery(), $query);
|
||||
$uri = $uri->withQuery(http_build_query(array_merge($query, (array) $options['query'])));
|
||||
$uri = $uri->withQuery(http_build_query(array_merge($query, (array) ($options['query'] ?? []))));
|
||||
|
||||
$body = '';
|
||||
$query = $uri->getQuery();
|
||||
@ -41,7 +45,7 @@ class Signature
|
||||
$nonce = Str::random();
|
||||
$path = '/'.ltrim($uri->getPath().(empty($query) ? '' : '?'.$query), '/');
|
||||
|
||||
if (! empty($options['body'])) {
|
||||
if (! empty($options['body']) && (is_scalar($options['body']) || $options['body'] instanceof Stringable)) {
|
||||
$body = strval($options['body']);
|
||||
}
|
||||
|
||||
|
||||
1
vendor/w7corp/easywechat/src/Pay/URLSchemeBuilder.php
vendored
Executable file → Normal file
1
vendor/w7corp/easywechat/src/Pay/URLSchemeBuilder.php
vendored
Executable file → Normal file
@ -7,6 +7,7 @@ namespace EasyWeChat\Pay;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use Exception;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
class URLSchemeBuilder
|
||||
|
||||
40
vendor/w7corp/easywechat/src/Pay/Utils.php
vendored
Executable file → Normal file
40
vendor/w7corp/easywechat/src/Pay/Utils.php
vendored
Executable file → Normal file
@ -2,14 +2,18 @@
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use function base64_encode;
|
||||
use function call_user_func_array;
|
||||
use const OPENSSL_PKCS1_OAEP_PADDING;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Kernel\Support\Str;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use EasyWeChat\Pay\Exceptions\EncryptionFailureException;
|
||||
use Exception;
|
||||
use function http_build_query;
|
||||
use JetBrains\PhpStorm\ArrayShape;
|
||||
|
||||
use function base64_encode;
|
||||
use function call_user_func_array;
|
||||
use function http_build_query;
|
||||
use function openssl_sign;
|
||||
use function strtoupper;
|
||||
use function time;
|
||||
@ -151,6 +155,34 @@ class Utils
|
||||
return base64_encode($signature);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://pay.weixin.qq.com/doc/v3/merchant/4013053257
|
||||
*
|
||||
* @param string $plaintext The text to be encrypted.
|
||||
* @param string|null $serial The serial number of the platform certificate to use for encryption. If null, the first available certificate will be used.
|
||||
* @return string The base64-encoded encrypted text.
|
||||
*
|
||||
* @throws InvalidConfigException If no platform certificate is found.
|
||||
* @throws EncryptionFailureException If the encryption process fails.
|
||||
*/
|
||||
public function encryptWithRsaPublicKey(string $plaintext, ?string $serial = null): string
|
||||
{
|
||||
$platformCerts = $this->merchant->getPlatformCerts();
|
||||
/** @var string $identifier - One of the serial number of the platform certificates OR the weixin pay's public key identifier. */
|
||||
$identifier = $serial ?? array_key_first($platformCerts);
|
||||
$platformCert = $this->merchant->getPlatformCert($identifier);
|
||||
|
||||
if (empty($platformCert)) {
|
||||
throw new InvalidConfigException('Missing platform certificate.');
|
||||
}
|
||||
|
||||
if (! openssl_public_encrypt($plaintext, $encrypted, $platformCert, OPENSSL_PKCS1_OAEP_PADDING)) {
|
||||
throw new EncryptionFailureException('Encrypt failed.');
|
||||
}
|
||||
|
||||
return base64_encode($encrypted);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidConfigException
|
||||
*/
|
||||
@ -163,7 +195,7 @@ class Utils
|
||||
throw new InvalidConfigException('Missing v2 secret key.');
|
||||
}
|
||||
|
||||
if ('HMAC-SHA256' === $params['signType']) {
|
||||
if ($params['signType'] === 'HMAC-SHA256') {
|
||||
$method = function ($str) use ($secretKey) {
|
||||
return hash_hmac('sha256', $str, $secretKey);
|
||||
};
|
||||
|
||||
71
vendor/w7corp/easywechat/src/Pay/Validator.php
vendored
Normal file
71
vendor/w7corp/easywechat/src/Pay/Validator.php
vendored
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace EasyWeChat\Pay;
|
||||
|
||||
use EasyWeChat\Kernel\Exceptions\InvalidConfigException;
|
||||
use EasyWeChat\Pay\Contracts\Merchant as MerchantInterface;
|
||||
use EasyWeChat\Pay\Exceptions\InvalidSignatureException;
|
||||
use Psr\Http\Message\MessageInterface;
|
||||
|
||||
class Validator implements \EasyWeChat\Pay\Contracts\Validator
|
||||
{
|
||||
public const MAX_ALLOWED_CLOCK_OFFSET = 300;
|
||||
|
||||
public const HEADER_TIMESTAMP = 'Wechatpay-Timestamp';
|
||||
|
||||
public const HEADER_NONCE = 'Wechatpay-Nonce';
|
||||
|
||||
public const HEADER_SERIAL = 'Wechatpay-Serial';
|
||||
|
||||
public const HEADER_SIGNATURE = 'Wechatpay-Signature';
|
||||
|
||||
public function __construct(protected MerchantInterface $merchant)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
|
||||
* @throws \EasyWeChat\Pay\Exceptions\InvalidSignatureException
|
||||
*/
|
||||
public function validate(MessageInterface $message): void
|
||||
{
|
||||
foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {
|
||||
if (! $message->hasHeader($header)) {
|
||||
throw new InvalidSignatureException("Missing Header: {$header}");
|
||||
}
|
||||
}
|
||||
|
||||
[$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);
|
||||
[$nonce] = $message->getHeader(self::HEADER_NONCE);
|
||||
[$serial] = $message->getHeader(self::HEADER_SERIAL);
|
||||
[$signature] = $message->getHeader(self::HEADER_SIGNATURE);
|
||||
|
||||
$body = (string) $message->getBody();
|
||||
|
||||
$message = "{$timestamp}\n{$nonce}\n{$body}\n";
|
||||
|
||||
if (\time() - \intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {
|
||||
throw new InvalidSignatureException('Clock Offset Exceeded');
|
||||
}
|
||||
|
||||
$publicKey = $this->merchant->getPlatformCert($serial);
|
||||
|
||||
if (! $publicKey) {
|
||||
throw new InvalidConfigException(
|
||||
"No platform certs found for serial: {$serial},
|
||||
please download from wechat pay and set it in merchant config with key `certs`."
|
||||
);
|
||||
}
|
||||
|
||||
if (\openssl_verify(
|
||||
$message,
|
||||
base64_decode($signature),
|
||||
strval($publicKey),
|
||||
OPENSSL_ALGO_SHA256
|
||||
) !== 1) {
|
||||
throw new InvalidSignatureException('Invalid Signature');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user