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
PHP

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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