You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

463 lines
14 KiB
PHTML

2 years ago
<?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));
}
}