2023-11-13 21:20:10 +08:00
<? php
namespace wchat\wx\V3 ;
use Exception ;
2023-11-13 22:13:44 +08:00
use Psr\Http\Message\RequestInterface ;
2023-11-13 21:20:10 +08:00
use wchat\wx\SmallProgram ;
use wchat\wx\V3\Notify\GoodsDetail ;
use wchat\wx\V3\Notify\NotifyModel ;
use wchat\wx\V3\Notify\PromotionDetail ;
2023-11-13 22:37:49 +08:00
const KEY_TYPE_PUBLIC = 'public' ;
const KEY_TYPE_PRIVATE = 'private' ;
2023-11-13 22:13:44 +08:00
const LOCAL_FILE_PROTOCOL = 'file://' ;
const PKEY_PEM_NEEDLE = ' KEY-' ;
const PKEY_PEM_FORMAT = "-----BEGIN %1 \$ s KEY----- \n %2 \$ s \n -----END %1 \$ s KEY-----" ;
const PKEY_PEM_FORMAT_PATTERN = '#-{5}BEGIN ((?:RSA )?(?:PUBLIC|PRIVATE)) KEY-{5}\r?\n([^-]+)\r?\n-{5}END \1 KEY-{5}#' ;
const CHR_CR = " \r " ;
const CHR_LF = " \n " ;
const RULES = [
2023-11-13 22:37:49 +08:00
'private.pkcs1' => [ PKEY_PEM_FORMAT , 'RSA PRIVATE' , 16 ],
'private.pkcs8' => [ PKEY_PEM_FORMAT , 'PRIVATE' , 16 ],
'public.pkcs1' => [ PKEY_PEM_FORMAT , 'RSA PUBLIC' , 15 ],
'public.spki' => [ PKEY_PEM_FORMAT , 'PUBLIC' , 14 ],
2023-11-13 22:13:44 +08:00
];
const ASN1_OID_RSAENCRYPTION = '300d06092a864886f70d0101010500' ;
const ASN1_SEQUENCE = 48 ;
const CHR_NUL = " \0 " ;
const CHR_ETX = " \3 " ;
2023-11-13 21:20:10 +08:00
class WxV3PaymentNotify extends SmallProgram
{
use WxV3PaymentTait ;
/**
* @param string $id
* @param string $create_time
* @param string $resource_type
* @param string $event_type
* @param string $summary
* @param array $resource
* @throws Exception
*/
public function __construct (
public string $id = "EV-2018022511223320873" ,
public string $create_time = "2015-05-20T13:29:35+08:00" ,
public string $resource_type = "encrypt-resource" ,
public string $event_type = "TRANSACTION.SUCCESS" ,
public string $summary = "支付成功" ,
public array $resource = []
)
{
}
/**
* @var NotifyModel
*/
public NotifyModel $notifyModel ;
2023-11-13 22:13:44 +08:00
/**
* @param RequestInterface $request
* @return bool
* @throws Exception
*/
public function verify ( RequestInterface $request ) : bool
{
$platformPublicKeyInstance = $this -> rsaFrom ( 'file:///path/to/wechatpay/inWechatpaySerial.pem' , KEY_TYPE_PUBLIC );
2023-11-13 22:42:26 +08:00
$inWechatpaySignature = $request -> getHeaderLine ( 'Wechatpay-Signature' ); // 请根据实际情况获取
$inWechatpayTimestamp = $request -> getHeaderLine ( 'Wechatpay-Timestamp' ); // 请根据实际情况获取
$inWechatpayNonce = $request -> getHeaderLine ( 'Wechatpay-Nonce' ); // 请根据实际情况获取
$inBody = $request -> getBody () -> getContents (); // 请根据实际情况获取,例如: file_get_contents('php://input');
2023-11-13 22:14:14 +08:00
$timeOffsetStatus = 300 >= abs ( time () - ( int ) $inWechatpayTimestamp );
$verifiedStatus = $this -> notifyVerify (
$this -> lineFeed ([ $inWechatpayTimestamp , $inWechatpayNonce , $inBody ]),
$inWechatpaySignature ,
$platformPublicKeyInstance );
2023-11-13 22:13:44 +08:00
if ( ! $timeOffsetStatus || ! $verifiedStatus ) {
return false ;
}
2023-11-13 22:37:49 +08:00
return $this -> decode ( $this -> resource [ 'ciphertext' ], $this -> resource [ 'nonce' ], $this -> resource [ 'associated_data' ]);
2023-11-13 22:13:44 +08:00
}
/**
* @param ...$pieces
* @return string
*/
protected function lineFeed ( ... $pieces ) : string
{
return implode ( " \n " , array_merge ( $pieces , [ '' ]));
}
/**
* @return string|bool
*/
protected function body () : string | bool
{
return json_encode ([ 'id' => $this -> id , 'create_time' => $this -> create_time , 'resource_type' => $this -> resource_type , 'event_type' => $this -> event_type , 'summary' => $this -> summary , 'resource' => $this -> resource ]);
}
/**
* @param string $message
* @param string $signature
* @param $publicKey
* @return bool
*/
protected function notifyVerify ( string $message , string $signature , $publicKey ) : bool
{
if (( $result = openssl_verify ( $message , base64_decode ( $signature ), $publicKey , OPENSSL_ALGO_SHA256 )) === false ) {
throw new \UnexpectedValueException ( 'Verified the input $message failed, please checking your $publicKey whether or nor correct.' );
}
return $result === 1 ;
}
/**
* @param $thing
* @param string $type
* @return \OpenSSLAsymmetricKey
*/
protected function rsaFrom ( $thing , string $type = KEY_TYPE_PRIVATE ) : \OpenSSLAsymmetricKey
{
2023-11-13 22:37:49 +08:00
$pkey = ( $isPublic = $type === KEY_TYPE_PUBLIC ) ? openssl_pkey_get_public ( $this -> parse ( $thing , $type )) : openssl_pkey_get_private ( $this -> parse ( $thing ));
2023-11-13 22:13:44 +08:00
if ( false === $pkey ) {
2023-11-13 22:37:49 +08:00
throw new \UnexpectedValueException ( sprintf ( 'Cannot load %s from(%s), please take care about the \$thing input.' , $isPublic ? 'publicKey' : 'privateKey' , gettype ( $thing )));
2023-11-13 22:13:44 +08:00
}
return $pkey ;
}
/**
* @param $thing
* @param string $type
* @return mixed|string
*/
protected function parse ( $thing , string $type = KEY_TYPE_PRIVATE ) : mixed
{
$src = $thing ;
2023-11-13 22:37:49 +08:00
if ( is_string ( $src ) && is_int ( strpos ( $src , PKEY_PEM_NEEDLE )) && $type === KEY_TYPE_PUBLIC && preg_match ( PKEY_PEM_FORMAT_PATTERN , $src , $matches )) {
2023-11-13 22:13:44 +08:00
[, $kind , $base64 ] = $matches ;
2023-11-13 22:37:49 +08:00
$mapRules = array_combine ( array_column ( RULES , 1 /*column*/ ), array_keys ( RULES ));
2023-11-13 22:13:44 +08:00
$protocol = $mapRules [ $kind ] ?? '' ;
if ( 'public.pkcs1' === $protocol ) {
$src = sprintf ( '%s://%s' , $protocol , str_replace ([ CHR_CR , CHR_LF ], '' , $base64 ));
}
}
if ( is_string ( $src ) && is_bool ( strpos ( $src , LOCAL_FILE_PROTOCOL )) && is_int ( strpos ( $src , '://' ))) {
$protocol = parse_url ( $src , PHP_URL_SCHEME );
[ $format , $kind , $offset ] = RULES [ $protocol ] ?? [ null , null , null ];
if ( $format && $kind && $offset ) {
$src = substr ( $src , $offset );
if ( 'public.pkcs1' === $protocol ) {
$src = $this -> pkcs1ToSpki ( $src );
[ $format , $kind ] = RULES [ 'public.spki' ];
}
return sprintf ( $format , $kind , wordwrap ( $src , 64 , CHR_LF , true ));
}
}
return $src ;
}
/**
* @param string $thing
* @return string
*/
protected function pkcs1ToSpki ( string $thing ) : string
{
$raw = CHR_NUL . base64_decode ( $thing );
$new = pack ( 'H*' , ASN1_OID_RSAENCRYPTION ) . CHR_ETX . self :: encodeLength ( $raw ) . $raw ;
return base64_encode ( pack ( 'Ca*a*' , ASN1_SEQUENCE , self :: encodeLength ( $new ), $new ));
}
/**
* @param string $thing
* @return string
*/
protected function encodeLength ( string $thing ) : string
{
$num = strlen ( $thing );
if ( $num <= 0x7F ) {
return sprintf ( '%c' , $num );
}
$tmp = ltrim ( pack ( 'N' , $num ), CHR_NUL );
return pack ( 'Ca*' , strlen ( $tmp ) | 0x80 , $tmp );
}
2023-11-13 21:20:10 +08:00
/**
2023-11-13 22:37:49 +08:00
* @param $ciphertext
* @param $nonce
* @param $associated_data
* @return bool
2023-11-13 21:20:10 +08:00
*/
2023-11-13 22:37:49 +08:00
public function decode ( $ciphertext , $nonce , $associated_data ) : bool
2023-11-13 21:20:10 +08:00
{
2023-11-13 22:37:49 +08:00
$data = $this -> decrypt ( $ciphertext , $nonce , $associated_data );
2023-11-13 21:20:10 +08:00
$this -> notifyModel = new NotifyModel ();
$this -> notifyModel -> amount = $data [ 'amount' ];
$this -> notifyModel -> payer = $data [ 'payer' ];
$this -> notifyModel -> scene_info = $data [ 'payer' ];
$this -> notifyModel -> appid = $data [ 'appid' ];
$this -> notifyModel -> mchid = $data [ 'mchid' ];
$this -> notifyModel -> out_trade_no = $data [ 'out_trade_no' ];
$this -> notifyModel -> transaction_id = $data [ 'transaction_id' ];
$this -> notifyModel -> trade_type = $data [ 'trade_type' ];
$this -> notifyModel -> trade_state = $data [ 'trade_state' ];
$this -> notifyModel -> trade_state_desc = $data [ 'trade_state_desc' ];
$this -> notifyModel -> bank_type = $data [ 'bank_type' ];
$this -> notifyModel -> attach = $data [ 'attach' ];
$this -> notifyModel -> success_time = $data [ 'success_time' ];
$this -> notifyModel -> promotion_detail = [];
foreach ( $data [ 'promotion_detail' ] as $datum ) {
$detail = new PromotionDetail ();
$detail -> amount = $datum [ 'amount' ];
$detail -> wechatpay_contribute = $datum [ 'wechatpay_contribute' ];
$detail -> coupon_id = $datum [ 'coupon_id' ];
$detail -> scope = $datum [ 'scope' ];
$detail -> merchant_contribute = $datum [ 'merchant_contribute' ];
$detail -> name = $datum [ 'name' ];
$detail -> other_contribute = $datum [ 'other_contribute' ];
$detail -> currency = $datum [ 'currency' ];
$detail -> stock_id = $datum [ 'stock_id' ];
$detail -> goods_detail = [];
foreach ( $datum [ 'goods_detail' ] as $value ) {
$detail -> goods_detail [] = new GoodsDetail ( $value );
}
$this -> notifyModel -> promotion_detail [] = $detail ;
}
2023-11-13 22:37:49 +08:00
return true ;
2023-11-13 21:20:10 +08:00
}
}