<?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));

    }

}