<?php
/**
 * 
 * @authors 清月曦 (1604583867@qq.com)
 * @date    2017-03-09 23:38:30
 * @version $Id$
 */
class Alipay
{
    // 配置信息在实例化时传入，以下为范例
    protected $config = [
        // // 即时到账方式
        // 'payment_type' => 1,
        // // 传输协议
        // 'transport' => 'http',
        // // 编码方式
        // 'input_charset' => 'utf-8',
        // // 签名方法
        // 'sign_type' => 'MD5',
        // // 支付完成异步通知调用地址
        // 'notify_url' => 'http://'.$_SERVER['HTTP_HOST'.'>/order/callback_alipay/notify',
        // // 支付完成同步返回地址
        // 'return_url' => 'http://'.$_SERVER['HTTP_HOST'.'>/order/callback_alipay/return',
        // // 证书路径
        // 'cacert' => APPPATH.'third_party/alipay/cacert.pem',
        // // 支付宝商家 ID
        // 'partner'      => '2088xxxxxxxx',
        // // 支付宝商家 KEY
        // 'key'          => 'xxxxxxxxxxxx',
        // // 支付宝商家注册邮箱
        // 'seller_email' => 'email@domain.com'
    ];
    private $is_mobile = FALSE; // 验证码图片实例
    
    private $service = 'create_direct_pay_by_user';
    private $service_wap = 'alipay.wap.trade.create.direct';

    private $alipay_gateway = 'https://mapi.alipay.com/gateway.do?';
    private $alipay_gateway_mobile = 'http://wappaygw.alipay.com/service/rest.htm?';
    
    private $verify_url = 'http://notify.alipay.com/trade/notify_query.do?';
    private $verify_url_https = 'https://mapi.alipay.com/gateway.do?service=notify_verify&';
    /**
     * 架构方法 设置参数
     * @access public
     * @param  array $config 配置参数
     */
    public function __construct($config = [], $is_mobile = FALSE)
    {
        $this->config = $config;
        
        $this->is_mobile = $is_mobile;
        if ($is_mobile) {
            $this->service = $this->service_wap;
            $this->alipay_gateway = $this->alipay_gateway_mobile;
        }
    }
    
    /**
     * 创建支付链接
     * @param $params
     * @return string
     */
    function buildRequestUrl($params)
    {
        $params = $this->buildSignedParameters($params);
        $url = $this->alipay_gateway_mobile . '&';

        foreach ($params as $key => $value) {
            $value = strval($value);
            $url .= $key . "=" . $value . "&";
        }

        //去掉最后一个&字符
        $url = substr($url, 0, count($url) - 2);
        return $url;
    }
    /**
     * 生成请求参数的发送表单HTML
     *
     * 其实这个函数没有必要，更应该使用签名后的参数自己组装，只不过有时候方便就从官方 SDK 里留下了。
     *
     * @param $params <Array> 请求参数（未签名的）
     * @param $method <String> 请求方法，默认：post，可选 get
     * @param $target <String> 提交目标，默认：_self
     *
     * @return <String>
     *
     */
    function buildRequestFormHTML($params, $method = 'post', $target = '_self')
    {
        $params = $this->buildSignedParameters($params);
        $html = "<form id='alipaysubmit' name='alipaysubmit' action='" . $this->alipay_gateway . "_input_charset=" . trim(strtolower($this->config['input_charset'])) . "' method='$method' target='$target'>";

        foreach ($params as $key => $value) {
            $html .= "<input type='hidden' name='$key' value='$value'/>";
        }

        $html .= "</form><script>document.forms['alipaysubmit'].submit();</script>";

        return $html;
    }
    /**
     * 支付完成验证返回参数（包含同步和异步）
     *
     * @param $async <Boolean> 是否异步通知模式
     *
     * @return <Boolean>
     */
    function verifyCallback()
    {
        $async = empty($_GET);

        $data = $async ? $_POST : $_GET;
        if (empty($data)) {
            return FALSE;
        }
        
        $signValid = $this->verifyParameters($data, $data["sign"]);

        if(isset($_POST['notify_data'])){
            $notify_data = simplest_xml_to_array($_POST['notify_data']);
            $notify_id = $notify_data['notify_id'];
        }else{
            $notify_id = $data['notify_id'];
        }

        if ($async && $this->is_mobile) {
            
            //对notify_data解密
            if ($this->config['sign_type'] == '0001') {
                $data['notify_data'] = $this->rsaDecrypt($data['notify_data'], $this->config['private_key_path']);
            }

            //notify_id从decrypt_post_para中解析出来（也就是说decrypt_post_para中已经包含notify_id的内容）
            $doc = new DOMDocument();
            $doc->loadXML($data['notify_data']);
            $notify_id = $doc->getElementsByTagName('notify_id')->item(0)->nodeValue;
        }
        //获取支付宝远程服务器ATN结果（验证是否是支付宝发来的消息）
        $responseTxt = 'true';
        if (!empty($notify_id)) {
            $responseTxt = $this->verifyFromServer($notify_id);
        }

        //验证
        //$signValid的结果不是true，与安全校验码、请求时的参数格式（如：带自定义参数等）、编码格式有关
        //$responsetTxt的结果不是true，与服务器设置问题、合作身份者ID、notify_id一分钟失效有关
        return $signValid && preg_match("/true$/i", $responseTxt);
    }
    function verifyParameters($params, $sign)
    {
        $params = $this->filterSignParameter($params);

        if (!$this->is_mobile) {
            ksort($params);
            reset($params);
        } else {
            $params = array(
                'service' => $params['service'],
                'v' => $params['v'],
                'sec_id' => $params['sec_id'],
                'notify_data' => $params['notify_data']
            );
        }

        $content = urldecode(http_build_query($params));
        switch (strtoupper(trim($this->config['sign_type']))) {
            case "MD5" :
                return md5($content . $this->config['key']) == $sign;

            case "RSA" :
            case "0001" :
                return $this->rsaVerify($content, $this->config['private_key_path'], $sign);

            default :
                return FALSE;
        }
    }
    function verifyFromServer($notify_id)
    {
        $transport = strtolower(trim($this->config['transport']));
        $partner = trim($this->config['partner']);
        $veryfy_url = ($transport == 'https' ? $this->verify_url_https : $this->verify_url) . "partner=$partner&notify_id=$notify_id";
        $curl = curl_init($veryfy_url);
        curl_setopt($curl, CURLOPT_HEADER, 0); // 过滤HTTP头
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl, CURLOPT_CAINFO, $this->config['cacert']);//证书地址
        $responseText = curl_exec($curl);
        // var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);
        return $responseText;
    }
    /**
     * 生成请求参数的签名
     *
     * @param $params <Array>
     *
     * @return <String>
     *
     * TODO:
     * 未考虑参数中空格被编码成加号“+”等情况
     */
    function signParameters($params)
    {
        // 支付宝的签名串必须是未经过 urlencode 的字符串
        // 不清楚为何 PHP 5.5 里没有 http_build_str() 方法
        $paramStr = urldecode(http_build_query($params));
        $result = "";
        switch (strtoupper(trim($this->config['sign_type']))) {
            case "MD5" :
                $result = md5($paramStr . $this->config['key']);
                break;

            case "RSA" :
            case "0001" :
                $priKey = file_get_contents($this->config['private_key_path']);
                $res = openssl_get_privatekey($priKey);
                openssl_sign($paramStr, $sign, $res);
                openssl_free_key($res);
                //base64编码
                $result = base64_encode($sign);
                break;

            default :
                $result = "";
        }

        return $result;
    }
    function filterSignParameter($params)
    {
        $result = array();
        foreach ($params as $key => $value) {
            if ($key != "sign" && $key != "sign_type" && $key != '_URL_' && $value != "") {
                $result[$key] = $value;
            }
        }
        return $result;
    }
    /**
     * 生成签名后的请求参数
     *
     * @param $params <Array>
     *        $params['out_trade_no']     唯一订单编号
     *        $params['subject']
     *        $params['total_fee']
     *        $params['body']
     *        $params['show_url']
     *        $params['anti_phishing_key']
     *        $params['exter_invoke_ip']
     *        $params['it_b_pay']
     *        $params['_input_charset']
     *
     * @return <Array>
     *
     */
    function buildSignedParameters($params)
    {
        $default = array(
            'service' => $this->service,
            'partner' => $this->config['partner']
        );

        if (!$this->is_mobile) {
            $default = array_merge($default, array(
                'payment_type' => $this->config['payment_type'],
                'seller_id' => $this->config['partner'],
                'notify_url' => $this->config['notify_url'],
                'return_url' => $this->config['return_url']
            ));
        }
        $params = $this->filterSignParameter(array_merge($default, $params));
        ksort($params);
        reset($params);

        $params['sign'] = $this->signParameters($params);
        if ($params['service'] != 'alipay.wap.trade.create.direct' && $params['service'] != 'alipay.wap.auth.authAndExecute') {
            $params['sign_type'] = strtoupper(trim($this->config['sign_type']));
        }

        return $params;
    }
    /**
     * 准备移动网页支付的请求参数
     *
     * 移动网页支付接口不同，需要先服务器提交一次请求，拿到返回 token 再返回客户端发起真实支付请求。
     * 该方法只完成第一次服务端请求，生成参数后需要客户端另行处理（可调用`buildRequestFormHTML`生成表单提交）。
     *
     * @param $params <Array>
     *        $params['out_trade_no'] 订单唯一编号
     *        $params['subject']      商品标题
     *        $params['total_fee']    支付总费用
     *        $params['merchant_url'] 商品链接地址
     *        $params['req_id']       请求唯一 ID
     *
     * @return <Array>
     */
    function prepareMobileTradeData($params)
    {
        // 不要用 SimpleXML 来构建 xml 结构，因为有第一行文档申明支付宝验证不通过
        $xml_str = '<direct_trade_create_req>' .
            '<notify_url>' . $this->config['notify_url'] . '</notify_url>' .
            '<call_back_url>' . $this->config['return_url'] . '</call_back_url>' .
            '<seller_account_name>' . $this->config['seller_email'] . '</seller_account_name>' .

            '<out_trade_no>' . $params['out_trade_no'] . '</out_trade_no>' .
            '<subject>' . htmlspecialchars($params['subject'], ENT_XML1, 'UTF-8') . '</subject>' .
            '<total_fee>' . $params['total_fee'] . '</total_fee>' .
            '<merchant_url>' . $params['merchant_url'] . '</merchant_url>' .
            '</direct_trade_create_req>';

        $request_data = $this->buildSignedParameters(array(
            'service' => $this->service,
            'partner' => $this->config['partner'],
            'sec_id' => $this->config['sign_type'],
            'format' => 'xml',
            'v' => '2.0',

            'req_id' => $params['req_id'],
            'req_data' => $xml_str,

            '_input_charset' => $this->config['input_charset']
        ));

        //远程获取数据
        $url = $this->alipay_gateway;
        $input_charset = trim(strtolower($this->config['input_charset']));
        $responseText = $this->getHttpResponsePOST($url, $this->config['cacert'],$request_data,$input_charset);

        parse_str($responseText, $responseData);
        if (!empty ($responseData['res_data'])) {

            if ($this->config['sign_type'] == '0001') {
                $responseData['res_data'] = rsaDecrypt($responseData['res_data'], $this->config['private_key_path']);
            }

            //token从res_data中解析出来（也就是说res_data中已经包含token的内容）
            $doc = new DOMDocument();
            $doc->loadXML($responseData['res_data']);
            $responseData['request_token'] = $doc->getElementsByTagName("request_token")->item(0)->nodeValue;
        }

        $xml_str = '<auth_and_execute_req>' .
            '<request_token>' . $responseData['request_token'] . '</request_token>' .
            '</auth_and_execute_req>';

        return array(
            'service' => 'alipay.wap.auth.authAndExecute',
            'partner' => $this->config['partner'],
            'sec_id' => $this->config['sign_type'],
            'format' => 'xml',
            'v' => '2.0',
            'req_data' => $xml_str
        );
    } 
    
    /**
     * 远程获取数据，POST模式
     * 注意：
     * 1.使用Crul需要修改服务器中php.ini文件的设置，找到php_curl.dll去掉前面的";"就行了
     * 2.文件夹中cacert.pem是SSL证书请保证其路径有效，目前默认路径是：getcwd().'\\cacert.pem'
     * @param $url 指定URL完整路径地址
     * @param $cacert_url 指定当前工作目录绝对路径
     * @param $para 请求的数据
     * @param $input_charset 编码格式。默认值：空值
     * return 远程输出的数据
     */
    function getHttpResponsePOST($url, $cacert_url, $para, $input_charset = '') {
    
        if (trim($input_charset) != '') {
            $url = $url."_input_charset=".$input_charset;
        }
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);//SSL证书认证
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);//严格认证
        curl_setopt($curl, CURLOPT_CAINFO,$cacert_url);//证书地址
        curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl,CURLOPT_POST,true); // post传输数据
        curl_setopt($curl,CURLOPT_POSTFIELDS,$para);// post传输数据
        $responseText = curl_exec($curl);
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常，可打开此开关，以便查看异常内容
        curl_close($curl);
        
        return $responseText;
    }
    
    /**
     * RSA验签
     * @param $data 待签名数据
     * @param $ali_public_key_path 支付宝的公钥文件路径
     * @param $sign 要校对的的签名结果
     * return 验证结果
     */
    function rsaVerify($data, $ali_public_key_path, $sign)
    {
        $pubKey = file_get_contents($ali_public_key_path);
        $res = openssl_get_publickey($pubKey);
        $result = (bool)openssl_verify($data, base64_decode($sign), $res);
        openssl_free_key($res);
        return $result;
    }

    /**
     * RSA解密
     * @param $content 需要解密的内容，密文
     * @param $private_key_path 商户私钥文件路径
     * return 解密后内容，明文
     */
    function rsaDecrypt($content, $private_key_path)
    {
        $priKey = file_get_contents($private_key_path);
        $res = openssl_get_privatekey($priKey);
        //用base64将内容还原成二进制
        $content = base64_decode($content);
        //把需要解密的内容，按128位拆开解密
        $result = '';
        for ($i = 0; $i < strlen($content) / 128; $i++) {
            $data = substr($content, $i * 128, 128);
            openssl_private_decrypt($data, $decrypt, $res);
            $result .= $decrypt;
        }
        openssl_free_key($res);
        return $result;
    }

    /**
     * 写日志，方便测试（看网站需求，也可以改成把记录存入数据库）
     * 注意：服务器需要开通fopen配置
     * @param $word 要写入日志里的文本内容 默认值：空值
     */
    function logResult($word = '')
    {
        $fp = fopen("./data/alipay_notify.log", "a");
        flock($fp, LOCK_EX);
        fwrite($fp, "执行日期：" . strftime("%Y%m%d%H%M%S", time()) . "\n" . $word . "\n");
        flock($fp, LOCK_UN);
        fclose($fp);
    }
}