<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: 麦当苗儿 <zuojiazi.cn@gmail.com> <http://www.zjzit.cn>
// +----------------------------------------------------------------------

namespace Com;
use Com\WechatCrypt;

//支持在非ThinkPHP环境下使用
defined('NOW_TIME') || define('NOW_TIME', $_SERVER['REQUEST_TIME']);
defined('IS_GET')   || define('IS_GET',   $_SERVER['REQUEST_METHOD'] == 'GET');

class Wechat {
    /**
     * 消息类型常量
     */
    const MSG_TYPE_TEXT       = 'text';
    const MSG_TYPE_IMAGE      = 'image';
    const MSG_TYPE_VOICE      = 'voice';
    const MSG_TYPE_VIDEO      = 'video';
    const MSG_TYPE_SHORTVIDEO = 'shortvideo';
    const MSG_TYPE_LOCATION   = 'location';
    const MSG_TYPE_LINK       = 'link';
    const MSG_TYPE_MUSIC      = 'music';
    const MSG_TYPE_NEWS       = 'news';
    const MSG_TYPE_EVENT      = 'event';

    /**
     * 事件类型常量
     */
    const MSG_EVENT_SUBSCRIBE   = 'subscribe';
    const MSG_EVENT_UNSUBSCRIBE = 'unsubscribe';
    const MSG_EVENT_SCAN        = 'SCAN';
    const MSG_EVENT_LOCATION    = 'LOCATION';
    const MSG_EVENT_CLICK       = 'CLICK';
    const MSG_EVENT_VIEW        = 'VIEW';

    /**
     * 微信推送过来的数据
     * @var array
     */
    private $data = array();

    /**
     * 微信TOKEN
     * @var string
     */
    private static $token = '';

    /**
     * 微信APP_ID
     * @var string
     */
    private static $appId = '';

    /**
     * 消息加密KEY
     * @var string
     */
    private static $encodingAESKey = '';

    /**
     * 是否使用安全模式
     * @var boolean
     */
    private static $msgSafeMode = false;

    /**
     * 构造方法,用于实例化微信SDK
     * 自动回复消息时实例化该SDK
     * @param string $token 微信后台填写的TOKEN
     * @param string $appid 微信APPID (安全模式和兼容模式有效)
     * @param string $key   消息加密KEY (EncodingAESKey)
     */
    public function __construct($token, $appid = '', $key = ''){
        //设置安全模式
        if(isset($_GET['encrypt_type']) && $_GET['encrypt_type'] == 'aes'){
            self::$msgSafeMode = true;
        }

        //参数验证
        if(self::$msgSafeMode){
            if(empty($key) || empty($appid)){
                throw new \Exception('缺少参数EncodingAESKey或APP_ID!');
            }

            self::$appId          = $appid;
            self::$encodingAESKey = $key;
        }

        //TOKEN验证
        if($token){
            self::auth($token) || exit;

            if(IS_GET){
                exit($_GET['echostr']);
            } else {
                self::$token = $token;
                $this->init();
            }
        } else {
            throw new \Exception('缺少参数TOKEN!');
        }
    }

    /**
     * 初始化微信推送的数据
     */
    private function init(){
        $xml  = file_get_contents("php://input");
        $data = self::xml2data($xml);

        //安全模式 或兼容模式
        if(self::$msgSafeMode){
            if(isset($data['MsgType'])){
                //兼容模式追加解密后的消息内容
                $data['Decrypt'] = self::extract($data['Encrypt']);
            } else { 
                //安全模式
                $data = self::extract($data['Encrypt']);
            }
        }

        $this->data = $data;
    }

    /**
     * 获取微信推送的数据
     * @return array 转换为数组后的数据
     */
    public function request(){
        return $this->data;
    }

    /**
     * * 响应微信发送的信息(自动回复)
     * @param  array  $content 回复信息,文本信息为string类型
     * @param  string $type    消息类型
     */
    public function response($content, $type = self::MSG_TYPE_TEXT){
        /* 基础数据 */
        $data = array(
            'ToUserName'   => $this->data['FromUserName'],
            'FromUserName' => $this->data['ToUserName'],
            'CreateTime'   => NOW_TIME,
            'MsgType'      => $type,
        );

        /* 按类型添加额外数据 */
        $content = call_user_func(array(self, $type), $content);
        if($type == self::MSG_TYPE_TEXT || $type == self::MSG_TYPE_NEWS){
            $data = array_merge($data, $content);
        } else {
            $data[ucfirst($type)] = $content;
        }

        //安全模式,加密消息内容
        if(self::$msgSafeMode){
            $data = self::generate($data);
        }

        /* 转换数据为XML */
        $xml = new \SimpleXMLElement('<xml></xml>');
        self::data2xml($xml, $data);
        exit($xml->asXML());
    }

    /**
     * 回复文本消息
     * @param  string $text   回复的文字
     */
    public function replyText($text){
        return $this->response($text, self::MSG_TYPE_TEXT);
    }

    /**
     * 回复图片消息
     * @param  string $media_id 图片ID
     */
    public function replyImage($media_id){
        return $this->response($media_id, self::MSG_TYPE_IMAGE);
    }

    /**
     * 回复语音消息
     * @param  string $media_id 音频ID
     */
    public function replyVoice($media_id){
        return $this->response($media_id, self::MSG_TYPE_VOICE);
    }

    /**
     * 回复视频消息
     * @param  string $media_id    视频ID
     * @param  string $title       视频标题
     * @param  string $discription 视频描述
     */
    public function replyVideo($media_id, $title, $discription){
        return $this->response(func_get_args(), self::MSG_TYPE_VIDEO);
    }

    /**
     * 回复音乐消息
     * @param  string $title          音乐标题
     * @param  string $discription    音乐描述
     * @param  string $musicurl       音乐链接
     * @param  string $hqmusicurl     高品质音乐链接
     * @param  string $thumb_media_id 缩略图ID
     */
    public function replyMusic($title, $discription, $musicurl, $hqmusicurl, $thumb_media_id){
        return $this->response(func_get_args(), self::MSG_TYPE_MUSIC);
    }

    /**
     * 回复图文消息,一个参数代表一条信息
     * @param  array  $news   图文内容 [标题,描述,URL,缩略图]
     * @param  array  $news1  图文内容 [标题,描述,URL,缩略图]
     * @param  array  $news2  图文内容 [标题,描述,URL,缩略图]
     *                ...     ...
     * @param  array  $news9  图文内容 [标题,描述,URL,缩略图]
     */
    public function replyNews($news, $news1, $news2, $news3){
        return $this->response(func_get_args(), self::MSG_TYPE_NEWS);
    }

    /**
     * 回复一条图文消息
     * @param  string $title       文章标题
     * @param  string $discription 文章简介
     * @param  string $url         文章连接
     * @param  string $picurl      文章缩略图
     */
    public function replyNewsOnce($title, $discription, $url, $picurl){
        return $this->response(array(func_get_args()), self::MSG_TYPE_NEWS);
    }

    /**
     * 数据XML编码
     * @param  object $xml  XML对象
     * @param  mixed  $data 数据
     * @param  string $item 数字索引时的节点名称
     * @return string
     */
    protected static function data2xml($xml, $data, $item = 'item') {
        foreach ($data as $key => $value) {
            /* 指定默认的数字key */
            is_numeric($key) && $key = $item;

            /* 添加子元素 */
            if(is_array($value) || is_object($value)){
                $child = $xml->addChild($key);
                self::data2xml($child, $value, $item);
            } else {
                if(is_numeric($value)){
                    $child = $xml->addChild($key, $value);
                } else {
                    $child = $xml->addChild($key);
                    $node  = dom_import_simplexml($child);
                    $cdata = $node->ownerDocument->createCDATASection($value);
                    $node->appendChild($cdata);
                }
            }
        }
    }

    /**
     * XML数据解码
     * @param  string $xml 原始XML字符串
     * @return array       解码后的数组
     */
    protected static function xml2data($xml){
        $xml = new \SimpleXMLElement($xml);
        
        if(!$xml){
            throw new \Exception('非法XXML');
        }

        $data = array();
        foreach ($xml as $key => $value) {
            $data[$key] = strval($value);
        }

        return $data;
    }

    /**
     * 对数据进行签名认证,确保是微信发送的数据
     * @param  string $token 微信开放平台设置的TOKEN
     * @return boolean       true-签名正确,false-签名错误
     */
    protected static function auth($token){
        /* 获取数据 */
        $data = array($_GET['timestamp'], $_GET['nonce'], $token);
        $sign = $_GET['signature'];
        
        /* 对数据进行字典排序 */
        sort($data, SORT_STRING);

        /* 生成签名 */
        $signature = sha1(implode($data));
        return $signature === $sign;
    }

    /**
     * 构造文本信息
     * @param  string $content 要回复的文本
     */
    private static function text($content){
        $data['Content'] = $content;
        return $data;
    }

    /**
     * 构造图片信息
     * @param  integer $media 图片ID
     */
    private static function image($media){
        $data['MediaId'] = $media;
        return $data;
    }

    /**
     * 构造音频信息
     * @param  integer $media 语音ID
     */
    private static function voice($media){
        $data['MediaId'] = $media;
        return $data;
    }

    /**
     * 构造视频信息
     * @param  array $video 要回复的视频 [视频ID,标题,说明]
     */
    private static function video($video){
        $data = array();
        list(
            $data['MediaId'],
            $data['Title'], 
            $data['Description'], 
        ) = $video;

        return $data;
    }

    /**
     * 构造音乐信息
     * @param  array $music 要回复的音乐[标题,说明,链接,高品质链接,缩略图ID]
     */
    private static function music($music){
        $data = array();
        list(
            $data['Title'], 
            $data['Description'], 
            $data['MusicUrl'], 
            $data['HQMusicUrl'],
            $data['ThumbMediaId'],
        ) = $music;

        return $data;
    }

    /**
     * 构造图文信息
     * @param  array $news 要回复的图文内容
     * [    
     *      0 => 第一条图文信息[标题,说明,图片链接,全文连接],
     *      1 => 第二条图文信息[标题,说明,图片链接,全文连接],
     *      2 => 第三条图文信息[标题,说明,图片链接,全文连接], 
     * ]
     */
    private static function news($news){
        $articles = array();
        foreach ($news as $key => $value) {
            list(
                $articles[$key]['Title'],
                $articles[$key]['Description'],
                $articles[$key]['Url'],
                $articles[$key]['PicUrl']
            ) = $value;

            if($key >= 9) break; //最多只允许10条图文信息
        }
        $data['ArticleCount'] = count($articles);
        $data['Articles']     = $articles;

        return $data;
    }

    /**
     * 验证并解密密文数据
     * @param  string $encrypt 密文
     * @return array           解密后的数据
     */
    private static function extract($encrypt){
        //验证数据签名
        $signature = self::sign($_GET['timestamp'], $_GET['nonce'], $encrypt);
        if($signature != $_GET['msg_signature']){
            throw new \Exception('数据签名错误!');
        }

        //消息解密对象
        $WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);

        //解密得到回明文消息
        $decrypt = $WechatCrypt->decrypt($encrypt);
        
        //返回解密的数据
        return self::xml2data($decrypt);
    }

    /**
     * 加密并生成密文消息数据
     * @param  array $data 获取到的加密的消息数据
     * @return array       生成的加密消息结构
     */
    private static function generate($data){
        /* 转换数据为XML */
        $xml = new \SimpleXMLElement('<xml></xml>');
        self::data2xml($xml, $data);
        $xml = $xml->asXML();

        //消息加密对象
        $WechatCrypt = new WechatCrypt(self::$encodingAESKey, self::$appId);

        //加密得到密文消息
        $encrypt = $WechatCrypt->encrypt($xml);

        //签名
        $nonce     = mt_rand(0, 9999999999);
        $signature = self::sign(NOW_TIME, $nonce, $encrypt);

        /* 加密消息基础数据 */
        $data = array(
            'Encrypt'      => $encrypt,
            'MsgSignature' => $signature,
            'TimeStamp'    => NOW_TIME,
            'Nonce'        => $nonce,
        );

        return $data;
    }

    /**
     * 生成数据签名
     * @param  string $timestamp 时间戳
     * @param  string $nonce     随机数
     * @param  string $encrypt   被签名的数据
     * @return string            SHA1签名
     */
    private static function sign($timestamp, $nonce, $encrypt){
        $sign  = array(self::$token, $timestamp, $nonce, $encrypt);
        sort($sign, SORT_STRING);
        return sha1(implode($sign));
    }
}