├── .travis.yml ├── LICENSE ├── README.md ├── example └── server.php ├── phpunit.xml.dist ├── src ├── Wechat.php ├── config.php ├── errorCode.php ├── pkcs7Encoder.php ├── sha1.php ├── wxBizMsgCrypt.php └── xmlparse.php └── test ├── EventTest.php ├── ExitTestHelper.php ├── GeneralTest.php ├── MyWechat.php ├── ReplyTest.php ├── SdkTestBase.php └── install-extensions.sh /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - ./test/install-extensions.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2013 NetPuter Lin http://netputer.me/ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 微信公众平台 PHP SDK 2 | ===== 3 | 4 | 介绍 5 | ----- 6 | 简单的微信公众平台 PHP SDK ,通过调用相应的接口,使你可以轻松地开发微信 App 。测试方法如下: 7 | 8 | 1. Clone 或下载项目源码,上传至服务器。 9 | 10 | 2. 进入[微信公众平台](https://mp.weixin.qq.com/),高级功能,开启开发模式,并设置接口配置信息。修改 `URL` 为 `/example/server.php` 的实际位置,修改 `Token` 为 `weixin` (可自行在 `/example/server.php` 中更改)。 11 | 12 | 3. 向你的微信公众号发送消息并测试吧! 13 | 14 | 用法 15 | ----- 16 | 17 | 直接浏览 `/example/server.php` 了解基本用法,以下为详细说明。 18 | 19 | 通过继承 `Wechat` 类进行扩展,通过重写 `onSubscribe()` 等方法响应关注等请求: 20 | 21 | ```php 22 | class MyWechat extends Wechat { 23 | protected function onSubscribe() {} // 用户关注 24 | protected function onUnsubscribe() {} // 用户取消关注 25 | 26 | protected function onText() { 27 | // 收到文本消息时触发,此处为响应代码 28 | } 29 | 30 | protected function onImage() {} // 收到图片消息 31 | protected function onLocation() {} // 收到地理位置消息 32 | protected function onLink() {} // 收到链接消息 33 | protected function onUnknown() {} // 收到未知类型消息 34 | } 35 | ``` 36 | ----- 37 | 38 | 使用 `getRequest()` 可以获取本次请求中的参数(不区分大小写): 39 | 40 | ```php 41 | $this->getRequest(); 42 | // 无参数时,返回包含所有参数的数组 43 | 44 | $this->getRequest('msgtype'); 45 | // 有参数且参数存在时,返回该参数的值 46 | 47 | $this->getRequest('ghost'); 48 | // 有参数但参数不存在时,返回 NULL 49 | ``` 50 | 51 | 所有请求均包含: 52 | 53 | ``` 54 | ToUserName 接收方帐号(该公众号ID) 55 | FromUserName 发送方帐号(代表用户的唯一标识) 56 | CreateTime 消息创建时间(时间戳) 57 | MsgId 消息ID(64位整型) 58 | ``` 59 | 60 | 文本消息请求: 61 | 62 | ``` 63 | MsgType text 64 | Content 文本消息内容 65 | ``` 66 | 67 | 图片消息请求: 68 | 69 | ``` 70 | MsgType image 71 | PicUrl 图片链接 72 | ``` 73 | 74 | 地理位置消息请求: 75 | 76 | ``` 77 | MsgType location 78 | Location_X 地理位置纬度 79 | Location_Y 地理位置经度 80 | Scale 地图缩放大小 81 | Label 地理位置信息 82 | ``` 83 | 84 | 链接消息请求: 85 | 86 | ``` 87 | MsgType link 88 | Title 消息标题 89 | Description 消息描述 90 | Url 消息链接 91 | ``` 92 | 93 | 事件推送: 94 | 95 | ``` 96 | MsgType event 97 | Event 事件类型 98 | EventKey 事件 Key 值,与自定义菜单接口中 Key 值对应 99 | ``` 100 | 101 | 其中,事件类型 `Event` 的值包括以下几种: 102 | 103 | ``` 104 | subscribe 关注 105 | unsubscribe 取消关注 106 | CLICK 自定义菜单点击事件(未验证) 107 | ``` 108 | ----- 109 | 110 | 使用 `responseText()` 方法回复文本消息: 111 | 112 | ```php 113 | $this->responseText( 114 | $content, // 消息内容 115 | $funcFlag // 可选参数(默认为0),设为1时星标刚才收到的消息 116 | ); 117 | ``` 118 | 119 | 使用 `responseMusic()` 方法回复音乐消息: 120 | 121 | ```php 122 | $this->responseMusic( 123 | $title, // 音乐标题 124 | $description, // 音乐描述 125 | $musicUrl, // 音乐链接 126 | $hqMusicUrl, // 高质量音乐链接,Wi-Fi 环境下优先使用 127 | $funcFlag // 可选参数,默认为0,设为1时星标刚才收到的消息 128 | ); 129 | ``` 130 | 131 | 使用 `responseNews()` 方法回复图文消息: 132 | 133 | ```php 134 | $this->responseNews( 135 | $items, // 由单条图文消息类型 NewsResponseItem() 组成的数组 136 | $funcFlag // 可选参数,默认为0,设为1时星标刚才收到的消息 137 | ) 138 | ``` 139 | 140 | 其中单条图文消息类型 `NewsResponseItem()` 格式如下: 141 | 142 | ```php 143 | $items[] = new NewsResponseItem( 144 | $title, // 图文消息标题 145 | $description, // 图文消息描述 146 | $picUrl, // 图片链接 147 | $url // 点击图文消息跳转链接 148 | ); 149 | ``` 150 | ----- 151 | 152 | 最后,实例化 `MyWechat()` 并调用 `run()` 方法即可运行。 153 | 154 | ```php 155 | $wechat = new MyWechat( 156 | $token, // 你在公众平台设置的 Token 157 | $debug // 调试模式,默认为 FALSE ,设为 TRUE 后可将错误通过文本消息回复显示 158 | ); 159 | 160 | $wechat->run(); 161 | ``` 162 | 163 | TODO 164 | ----- 165 | 1. 完善文档和注释; 166 | 2. 完善异常处理; 167 | 3. 提供 Composer 方式安装。 168 | -------------------------------------------------------------------------------- /example/server.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | 8 | require('../src/Wechat.php'); 9 | 10 | /** 11 | * 微信公众平台演示类 12 | */ 13 | class MyWechat extends Wechat { 14 | 15 | /** 16 | * 用户关注时触发,回复「欢迎关注」 17 | * 18 | * @return void 19 | */ 20 | protected function onSubscribe() { 21 | $this->responseText('欢迎关注'); 22 | } 23 | 24 | /** 25 | * 用户已关注时,扫描带参数二维码时触发,回复二维码的EventKey (测试帐号似乎不能触发) 26 | * 27 | * @return void 28 | */ 29 | protected function onScan() { 30 | $this->responseText('二维码的EventKey:' . $this->getRequest('EventKey')); 31 | } 32 | 33 | /** 34 | * 用户取消关注时触发 35 | * 36 | * @return void 37 | */ 38 | protected function onUnsubscribe() { 39 | // 「悄悄的我走了,正如我悄悄的来;我挥一挥衣袖,不带走一片云彩。」 40 | } 41 | 42 | /** 43 | * 上报地理位置时触发,回复收到的地理位置 44 | * 45 | * @return void 46 | */ 47 | protected function onEventLocation() { 48 | $this->responseText('收到了位置推送:' . $this->getRequest('Latitude') . ',' . $this->getRequest('Longitude')); 49 | } 50 | 51 | /** 52 | * 收到文本消息时触发,回复收到的文本消息内容 53 | * 54 | * @return void 55 | */ 56 | protected function onText() { 57 | $this->responseText('收到了文字消息:' . $this->getRequest('content')); 58 | } 59 | 60 | /** 61 | * 收到图片消息时触发,回复由收到的图片组成的图文消息 62 | * 63 | * @return void 64 | */ 65 | protected function onImage() { 66 | $items = array( 67 | new NewsResponseItem('标题一', '描述一', $this->getRequest('picurl'), $this->getRequest('picurl')), 68 | new NewsResponseItem('标题二', '描述二', $this->getRequest('picurl'), $this->getRequest('picurl')), 69 | ); 70 | 71 | $this->responseNews($items); 72 | } 73 | 74 | /** 75 | * 收到地理位置消息时触发,回复收到的地理位置 76 | * 77 | * @return void 78 | */ 79 | protected function onLocation() { 80 | $num = 1 / 0; 81 | // 故意触发错误,用于演示调试功能 82 | 83 | $this->responseText('收到了位置消息:' . $this->getRequest('location_x') . ',' . $this->getRequest('location_y')); 84 | } 85 | 86 | /** 87 | * 收到链接消息时触发,回复收到的链接地址 88 | * 89 | * @return void 90 | */ 91 | protected function onLink() { 92 | $this->responseText('收到了链接:' . $this->getRequest('url')); 93 | } 94 | 95 | /** 96 | * 收到语音消息时触发,回复语音识别结果(需要开通语音识别功能) 97 | * 98 | * @return void 99 | */ 100 | protected function onVoice() { 101 | $this->responseText('收到了语音消息,识别结果为:' . $this->getRequest('Recognition')); 102 | } 103 | 104 | /** 105 | * 收到自定义菜单消息时触发,回复菜单的EventKey 106 | * 107 | * @return void 108 | */ 109 | protected function onClick() { 110 | $this->responseText('你点击了菜单:' . $this->getRequest('EventKey')); 111 | } 112 | 113 | /** 114 | * 收到未知类型消息时触发,回复收到的消息类型 115 | * 116 | * @return void 117 | */ 118 | protected function onUnknown() { 119 | $this->responseText('收到了未知类型消息:' . $this->getRequest('msgtype')); 120 | } 121 | 122 | } 123 | 124 | $wechat = new MyWechat(array( 125 | 'token' => $token, 126 | 'aeskey' => $encodingAesKey, 127 | 'appid' => $appId, 128 | 'debug' => $debugMode 129 | )); 130 | $wechat->run(); 131 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./test 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Wechat.php: -------------------------------------------------------------------------------- 1 | 6 | */ 7 | include_once "wxBizMsgCrypt.php"; 8 | include_once "config.php"; 9 | /** 10 | * 微信公众平台处理类 11 | */ 12 | class Wechat { 13 | 14 | /** 15 | * 调试模式,将错误通过文本消息回复显示 16 | * 17 | * @var boolean 18 | */ 19 | private $debug; 20 | 21 | /** 22 | * 以数组的形式保存微信服务器每次发来的请求 23 | * 24 | * @var array 25 | */ 26 | private $request; 27 | 28 | /** 29 | * WXBizMsgCrypt 30 | * 31 | * @var WXBizMsgCrypt 32 | */ 33 | private $msgCryptor; 34 | 35 | /** 36 | * If msg is in crypt mode 37 | * 38 | * @var boolean 39 | */ 40 | private $encrypted = false; 41 | 42 | /** 43 | * Store post data from wechat server 44 | * 45 | * @var string 46 | */ 47 | private $postStr; 48 | 49 | /** 50 | * 初始化,判断此次请求是否为验证请求,并以数组形式保存 51 | * 52 | * @param string $token 验证信息 53 | * @param boolean $debug 调试模式,默认为关闭 54 | */ 55 | public function __construct($config=array('token'=>'', 'aeskey'=>'', 'appid'=>'', 'debug' => FALSE)) { 56 | 57 | $token = $config['token']; 58 | $aeskey = $config['aeskey']; 59 | $appid = $config['appid']; 60 | $debug = $config['debug']; 61 | 62 | if (!$this->validateSignature($token)) { 63 | exit('签名验证失败'); 64 | } 65 | 66 | if ($this->isValidateIncomingConn()) { 67 | // 网址接入验证 68 | exit($_GET['echostr']); 69 | } 70 | 71 | if ($_SERVER['REQUEST_METHOD'] == "POST") { 72 | $this->postStr = file_get_contents("php://input"); 73 | } 74 | if (!isset($this->postStr)) { 75 | exit('缺少数据'); 76 | } 77 | 78 | $this->debug = $debug; 79 | set_error_handler(array(&$this, 'errorHandler')); 80 | // 设置错误处理函数,将错误通过文本消息回复显示 81 | 82 | if (isset($_GET['encrypt_type'])) { 83 | $this->encrypted = $_GET['encrypt_type'] == 'aes'; 84 | } 85 | 86 | if ($this->encrypted) { 87 | $this->msgCryptor = new wxBizMsgCrypt($token, $aeskey, $appid); 88 | } 89 | 90 | $this->savePostData(); 91 | 92 | } 93 | 94 | private function savePostData() { 95 | $xml = ''; 96 | 97 | if ($this->encrypted) { 98 | $errCode = $this->msgCryptor->decryptMsg($_GET['msg_signature'], $_GET['timestamp'], $_GET['nonce'], $this->postStr, $xml); 99 | 100 | if ($errCode != 0) exit($errCode); 101 | 102 | } else { 103 | $xml = $this->postStr; 104 | } 105 | 106 | $xml = (array) simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); 107 | 108 | $this->request = array_change_key_case($xml, CASE_LOWER); 109 | // 将数组键名转换为小写,提高健壮性,减少因大小写不同而出现的问题 110 | } 111 | 112 | /** 113 | * 判断此次请求是否为验证请求 114 | * 115 | * @return boolean 116 | */ 117 | private function isValidateIncomingConn() { 118 | return isset($_GET['echostr']); 119 | } 120 | 121 | /** 122 | * 验证此次请求的签名信息 123 | * 124 | * @param string $token 验证信息 125 | * @return boolean 126 | */ 127 | private function validateSignature($token) { 128 | if ( ! (isset($_GET['signature']) && isset($_GET['timestamp']) && isset($_GET['nonce']))) { 129 | return FALSE; 130 | } 131 | 132 | $signature = $_GET['signature']; 133 | $timestamp = $_GET['timestamp']; 134 | $nonce = $_GET['nonce']; 135 | 136 | $signatureArray = array($token, $timestamp, $nonce); 137 | sort($signatureArray,SORT_STRING); 138 | 139 | return sha1(implode($signatureArray)) == $signature; 140 | } 141 | 142 | /** 143 | * 获取本次请求中的参数,不区分大小 144 | * 145 | * @param string $param 参数名,默认为无参 146 | * @return mixed 147 | */ 148 | protected function getRequest($param = FALSE) { 149 | if ($param === FALSE) { 150 | return $this->request; 151 | } 152 | 153 | $param = strtolower($param); 154 | 155 | if (isset($this->request[$param])) { 156 | return $this->request[$param]; 157 | } 158 | 159 | return NULL; 160 | } 161 | 162 | /** 163 | * 用户关注时触发,用于子类重写 164 | * 165 | * @return void 166 | */ 167 | protected function onSubscribe() {} 168 | 169 | /** 170 | * 用户取消关注时触发,用于子类重写 171 | * 172 | * @return void 173 | */ 174 | protected function onUnsubscribe() {} 175 | 176 | /** 177 | * 收到文本消息时触发,用于子类重写 178 | * 179 | * @return void 180 | */ 181 | protected function onText() {} 182 | 183 | /** 184 | * 收到图片消息时触发,用于子类重写 185 | * 186 | * @return void 187 | */ 188 | protected function onImage() {} 189 | 190 | /** 191 | * 收到地理位置消息时触发,用于子类重写 192 | * 193 | * @return void 194 | */ 195 | protected function onLocation() {} 196 | 197 | /** 198 | * 收到链接消息时触发,用于子类重写 199 | * 200 | * @return void 201 | */ 202 | protected function onLink() {} 203 | 204 | /** 205 | * 收到自定义菜单消息时触发,用于子类重写 206 | * 207 | * @return void 208 | */ 209 | protected function onClick() {} 210 | 211 | /** 212 | * 收到地理位置事件消息时触发,用于子类重写 213 | * 214 | * @return void 215 | */ 216 | protected function onEventLocation() {} 217 | 218 | /** 219 | * 收到语音消息时触发,用于子类重写 220 | * 221 | * @return void 222 | */ 223 | protected function onVoice() {} 224 | 225 | /** 226 | * 扫描二维码时触发,用于子类重写 227 | * 228 | * @return void 229 | */ 230 | protected function onScan() {} 231 | 232 | /** 233 | * 收到未知类型消息时触发,用于子类重写 234 | * 235 | * @return void 236 | */ 237 | protected function onUnknown() {} 238 | 239 | 240 | /** 241 | * 输出消息 242 | * @return [encypted] msg 243 | */ 244 | 245 | private function sendout($msg) { 246 | if ($this->encrypted) { 247 | $errCode = $this->msgCryptor->encryptMsg($msg, $this->getRequest('timestamp'), $this->getRequest('nonce'), $msg); 248 | if ($errCode != 0) exit ($errCode); 249 | } 250 | exit($msg); 251 | } 252 | 253 | /** 254 | * 回复文本消息 255 | * 256 | * @param string $content 消息内容 257 | * @param integer $funcFlag 默认为0,设为1时星标刚才收到的消息 258 | * @return void 259 | */ 260 | protected function responseText($content, $funcFlag = 0) { 261 | $this->sendout(new TextResponse($this->getRequest('fromusername'), $this->getRequest('tousername'), $content, $funcFlag)); 262 | } 263 | 264 | /** 265 | * 回复音乐消息 266 | * 267 | * @param string $title 音乐标题 268 | * @param string $description 音乐描述 269 | * @param string $musicUrl 音乐链接 270 | * @param string $hqMusicUrl 高质量音乐链接,Wi-Fi 环境下优先使用 271 | * @param integer $funcFlag 默认为0,设为1时星标刚才收到的消息 272 | * @return void 273 | */ 274 | protected function responseMusic($title, $description, $musicUrl, $hqMusicUrl, $funcFlag = 0) { 275 | $this->sendout(new MusicResponse($this->getRequest('fromusername'), $this->getRequest('tousername'), $title, $description, $musicUrl, $hqMusicUrl, $funcFlag)); 276 | } 277 | 278 | /** 279 | * 回复图文消息 280 | * @param array $items 由单条图文消息类型 NewsResponseItem() 组成的数组 281 | * @param integer $funcFlag 默认为0,设为1时星标刚才收到的消息 282 | * @return void 283 | */ 284 | protected function responseNews($items, $funcFlag = 0) { 285 | $this->sendout(new NewsResponse($this->getRequest('fromusername'), $this->getRequest('tousername'), $items, $funcFlag)); 286 | } 287 | 288 | /** 289 | * 分析消息类型,并分发给对应的函数 290 | * 291 | * @return void 292 | */ 293 | public function run() { 294 | switch ($this->getRequest('msgtype')) { 295 | 296 | case 'event': 297 | switch ($this->getRequest('event')) { 298 | 299 | case 'subscribe': 300 | $this->onSubscribe(); 301 | break; 302 | 303 | case 'unsubscribe': 304 | $this->onUnsubscribe(); 305 | break; 306 | 307 | case 'SCAN': 308 | $this->onScan(); 309 | break; 310 | 311 | case 'LOCATION': 312 | $this->onEventLocation(); 313 | break; 314 | 315 | case 'CLICK': 316 | $this->onClick(); 317 | break; 318 | 319 | } 320 | 321 | break; 322 | 323 | case 'text': 324 | $this->onText(); 325 | break; 326 | 327 | case 'image': 328 | $this->onImage(); 329 | break; 330 | 331 | case 'location': 332 | $this->onLocation(); 333 | break; 334 | 335 | case 'link': 336 | $this->onLink(); 337 | break; 338 | 339 | case 'voice': 340 | $this->onVoice(); 341 | break; 342 | 343 | default: 344 | $this->onUnknown(); 345 | break; 346 | 347 | } 348 | } 349 | 350 | /** 351 | * 自定义的错误处理函数,将 PHP 错误通过文本消息回复显示 352 | * @param int $level 错误代码 353 | * @param string $msg 错误内容 354 | * @param string $file 产生错误的文件 355 | * @param int $line 产生错误的行数 356 | * @return void 357 | */ 358 | public function errorHandler($level, $msg, $file, $line) { 359 | if ( ! $this->debug) { 360 | return; 361 | } 362 | 363 | $error_type = array( 364 | // E_ERROR => 'Error', 365 | E_WARNING => 'Warning', 366 | // E_PARSE => 'Parse Error', 367 | E_NOTICE => 'Notice', 368 | // E_CORE_ERROR => 'Core Error', 369 | // E_CORE_WARNING => 'Core Warning', 370 | // E_COMPILE_ERROR => 'Compile Error', 371 | // E_COMPILE_WARNING => 'Compile Warning', 372 | E_USER_ERROR => 'User Error', 373 | E_USER_WARNING => 'User Warning', 374 | E_USER_NOTICE => 'User Notice', 375 | E_STRICT => 'Strict', 376 | E_RECOVERABLE_ERROR => 'Recoverable Error', 377 | E_DEPRECATED => 'Deprecated', 378 | E_USER_DEPRECATED => 'User Deprecated', 379 | ); 380 | 381 | $template = <<responseText(sprintf($template, 390 | $error_type[$level], 391 | $msg, 392 | $file, 393 | $line 394 | )); 395 | } 396 | 397 | } 398 | 399 | /** 400 | * 用于回复的基本消息类型 401 | */ 402 | abstract class WechatResponse { 403 | 404 | protected $toUserName; 405 | protected $fromUserName; 406 | protected $funcFlag; 407 | protected $template; 408 | 409 | public function __construct($toUserName, $fromUserName, $funcFlag) { 410 | $this->toUserName = $toUserName; 411 | $this->fromUserName = $fromUserName; 412 | $this->funcFlag = $funcFlag; 413 | } 414 | 415 | abstract public function __toString(); 416 | 417 | } 418 | 419 | /** 420 | * 用于回复的文本消息类型 421 | */ 422 | class TextResponse extends WechatResponse { 423 | 424 | protected $content; 425 | 426 | public function __construct($toUserName, $fromUserName, $content, $funcFlag = 0) { 427 | parent::__construct($toUserName, $fromUserName, $funcFlag); 428 | 429 | $this->content = $content; 430 | $this->template = << 432 | 433 | 434 | %s 435 | 436 | 437 | %s 438 | 439 | XML; 440 | } 441 | 442 | public function __toString() { 443 | return sprintf($this->template, 444 | $this->toUserName, 445 | $this->fromUserName, 446 | time(), 447 | $this->content, 448 | $this->funcFlag 449 | ); 450 | } 451 | 452 | } 453 | 454 | /** 455 | * 用于回复的音乐消息类型 456 | */ 457 | class MusicResponse extends WechatResponse { 458 | 459 | protected $title; 460 | protected $description; 461 | protected $musicUrl; 462 | protected $hqMusicUrl; 463 | 464 | public function __construct($toUserName, $fromUserName, $title, $description, $musicUrl, $hqMusicUrl, $funcFlag) { 465 | parent::__construct($toUserName, $fromUserName, $funcFlag); 466 | 467 | $this->title = $title; 468 | $this->description = $description; 469 | $this->musicUrl = $musicUrl; 470 | $this->hqMusicUrl = $hqMusicUrl; 471 | $this->template = << 473 | 474 | 475 | %s 476 | 477 | 478 | <![CDATA[%s]]> 479 | 480 | 481 | 482 | 483 | %s 484 | 485 | XML; 486 | } 487 | 488 | public function __toString() { 489 | return sprintf($this->template, 490 | $this->toUserName, 491 | $this->fromUserName, 492 | time(), 493 | $this->title, 494 | $this->description, 495 | $this->musicUrl, 496 | $this->hqMusicUrl, 497 | $this->funcFlag 498 | ); 499 | } 500 | 501 | } 502 | 503 | /** 504 | * 用于回复的图文消息类型 505 | */ 506 | class NewsResponse extends WechatResponse { 507 | 508 | protected $items = array(); 509 | 510 | public function __construct($toUserName, $fromUserName, $items, $funcFlag) { 511 | parent::__construct($toUserName, $fromUserName, $funcFlag); 512 | 513 | $this->items = $items; 514 | $this->template = << 516 | 517 | 518 | %s 519 | 520 | %s 521 | 522 | %s 523 | 524 | %s 525 | 526 | XML; 527 | } 528 | 529 | public function __toString() { 530 | return sprintf($this->template, 531 | $this->toUserName, 532 | $this->fromUserName, 533 | time(), 534 | count($this->items), 535 | implode($this->items), 536 | $this->funcFlag 537 | ); 538 | } 539 | 540 | } 541 | 542 | /** 543 | * 单条图文消息类型 544 | */ 545 | class NewsResponseItem { 546 | 547 | protected $title; 548 | protected $description; 549 | protected $picUrl; 550 | protected $url; 551 | protected $template; 552 | 553 | public function __construct($title, $description, $picUrl, $url) { 554 | $this->title = $title; 555 | $this->description = $description; 556 | $this->picUrl = $picUrl; 557 | $this->url = $url; 558 | $this->template = << 560 | <![CDATA[%s]]> 561 | 562 | 563 | 564 | 565 | XML; 566 | } 567 | 568 | public function __toString() { 569 | return sprintf($this->template, 570 | $this->title, 571 | $this->description, 572 | $this->picUrl, 573 | $this->url 574 | ); 575 | } 576 | 577 | } 578 | -------------------------------------------------------------------------------- /src/config.php: -------------------------------------------------------------------------------- 1 | 6 | *
  • -40001: 签名验证错误
  • 7 | *
  • -40002: xml解析失败
  • 8 | *
  • -40003: sha加密生成签名失败
  • 9 | *
  • -40004: encodingAesKey 非法
  • 10 | *
  • -40005: appid 校验错误
  • 11 | *
  • -40006: aes 加密失败
  • 12 | *
  • -40007: aes 解密失败
  • 13 | *
  • -40008: 解密后得到的buffer非法
  • 14 | *
  • -40009: base64加密失败
  • 15 | *
  • -40010: base64解密失败
  • 16 | *
  • -40011: 生成xml失败
  • 17 | * 18 | */ 19 | class ErrorCode 20 | { 21 | static $OK = 0; 22 | static $ValidateSignatureError = -40001; 23 | static $ParseXmlError = -40002; 24 | static $ComputeSignatureError = -40003; 25 | static $IllegalAesKey = -40004; 26 | static $ValidateAppidError = -40005; 27 | static $EncryptAESError = -40006; 28 | static $DecryptAESError = -40007; 29 | static $IllegalBuffer = -40008; 30 | static $EncodeBase64Error = -40009; 31 | static $DecodeBase64Error = -40010; 32 | static $GenReturnXmlError = -40011; 33 | } 34 | -------------------------------------------------------------------------------- /src/pkcs7Encoder.php: -------------------------------------------------------------------------------- 1 | 32) { 46 | $pad = 0; 47 | } 48 | return substr($text, 0, (strlen($text) - $pad)); 49 | } 50 | 51 | } 52 | 53 | /** 54 | * Prpcrypt class 55 | * 56 | * 提供接收和推送给公众平台消息的加解密接口. 57 | */ 58 | class Prpcrypt 59 | { 60 | public $key; 61 | 62 | function Prpcrypt($k) 63 | { 64 | $this->key = base64_decode($k . "="); 65 | } 66 | 67 | /** 68 | * 对明文进行加密 69 | * @param string $text 需要加密的明文 70 | * @return string 加密后的密文 71 | */ 72 | public function encrypt($text, $appid) 73 | { 74 | 75 | try { 76 | //获得16位随机字符串,填充到明文之前 77 | $random = $this->getRandomStr(); 78 | $text = $random . pack("N", strlen($text)) . $text . $appid; 79 | // 网络字节序 80 | $size = mcrypt_get_block_size(MCRYPT_RIJNDAEL_128, MCRYPT_MODE_CBC); 81 | $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); 82 | $iv = substr($this->key, 0, 16); 83 | //使用自定义的填充方式对明文进行补位填充 84 | $pkc_encoder = new PKCS7Encoder; 85 | $text = $pkc_encoder->encode($text); 86 | mcrypt_generic_init($module, $this->key, $iv); 87 | //加密 88 | $encrypted = mcrypt_generic($module, $text); 89 | mcrypt_generic_deinit($module); 90 | mcrypt_module_close($module); 91 | //print(base64_encode($encrypted)); 92 | //使用BASE64对加密后的字符串进行编码 93 | return array(ErrorCode::$OK, base64_encode($encrypted)); 94 | } catch (Exception $e) { 95 | //print $e; 96 | return array(ErrorCode::$EncryptAESError, null); 97 | } 98 | } 99 | 100 | /** 101 | * 对密文进行解密 102 | * @param string $encrypted 需要解密的密文 103 | * @return string 解密得到的明文 104 | */ 105 | public function decrypt($encrypted, $appid) 106 | { 107 | 108 | try { 109 | //使用BASE64对需要解密的字符串进行解码 110 | $ciphertext_dec = base64_decode($encrypted); 111 | $module = mcrypt_module_open(MCRYPT_RIJNDAEL_128, '', MCRYPT_MODE_CBC, ''); 112 | $iv = substr($this->key, 0, 16); 113 | mcrypt_generic_init($module, $this->key, $iv); 114 | 115 | //解密 116 | $decrypted = mdecrypt_generic($module, $ciphertext_dec); 117 | mcrypt_generic_deinit($module); 118 | mcrypt_module_close($module); 119 | } catch (Exception $e) { 120 | return array(ErrorCode::$DecryptAESError, null); 121 | } 122 | 123 | 124 | try { 125 | //去除补位字符 126 | $pkc_encoder = new PKCS7Encoder; 127 | $result = $pkc_encoder->decode($decrypted); 128 | //去除16位随机字符串,网络字节序和AppId 129 | if (strlen($result) < 16) 130 | return ""; 131 | $content = substr($result, 16, strlen($result)); 132 | $len_list = unpack("N", substr($content, 0, 4)); 133 | $xml_len = $len_list[1]; 134 | $xml_content = substr($content, 4, $xml_len); 135 | $from_appid = substr($content, $xml_len + 4); 136 | } catch (Exception $e) { 137 | //print $e; 138 | return array(ErrorCode::$IllegalBuffer, null); 139 | } 140 | if ($from_appid != $appid) 141 | return array(ErrorCode::$ValidateAppidError, null); 142 | return array(0, $xml_content); 143 | 144 | } 145 | 146 | 147 | /** 148 | * 随机生成16位字符串 149 | * @return string 生成的字符串 150 | */ 151 | function getRandomStr() 152 | { 153 | 154 | $str = ""; 155 | $str_pol = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"; 156 | $max = strlen($str_pol) - 1; 157 | for ($i = 0; $i < 16; $i++) { 158 | $str .= $str_pol[mt_rand(0, $max)]; 159 | } 160 | return $str; 161 | } 162 | 163 | } 164 | -------------------------------------------------------------------------------- /src/sha1.php: -------------------------------------------------------------------------------- 1 | token = $token; 34 | $this->encodingAesKey = $encodingAesKey; 35 | $this->appId = $appId; 36 | } 37 | 38 | /** 39 | * 将公众平台回复用户的消息加密打包. 40 | *
      41 | *
    1. 对要发送的消息进行AES-CBC加密
    2. 42 | *
    3. 生成安全签名
    4. 43 | *
    5. 将消息密文和安全签名打包成xml格式
    6. 44 | *
    45 | * 46 | * @param $replyMsg string 公众平台待回复用户的消息,xml格式的字符串 47 | * @param $timeStamp string 时间戳,可以自己生成,也可以用URL参数的timestamp 48 | * @param $nonce string 随机串,可以自己生成,也可以用URL参数的nonce 49 | * @param &$encryptMsg string 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串, 50 | * 当return返回0时有效 51 | * 52 | * @return int 成功0,失败返回对应的错误码 53 | */ 54 | public function encryptMsg($replyMsg, $timeStamp, $nonce, &$encryptMsg) 55 | { 56 | $pc = new Prpcrypt($this->encodingAesKey); 57 | 58 | //加密 59 | $array = $pc->encrypt($replyMsg, $this->appId); 60 | $ret = $array[0]; 61 | if ($ret != 0) { 62 | return $ret; 63 | } 64 | 65 | if ($timeStamp == null) { 66 | $timeStamp = time(); 67 | } 68 | $encrypt = $array[1]; 69 | 70 | //生成安全签名 71 | $sha1 = new SHA1; 72 | $array = $sha1->getSHA1($this->token, $timeStamp, $nonce, $encrypt); 73 | $ret = $array[0]; 74 | if ($ret != 0) { 75 | return $ret; 76 | } 77 | $signature = $array[1]; 78 | 79 | //生成发送的xml 80 | $xmlparse = new XMLParse; 81 | $encryptMsg = $xmlparse->generate($encrypt, $signature, $timeStamp, $nonce); 82 | return ErrorCode::$OK; 83 | } 84 | 85 | 86 | /** 87 | * 检验消息的真实性,并且获取解密后的明文. 88 | *
      89 | *
    1. 利用收到的密文生成安全签名,进行签名验证
    2. 90 | *
    3. 若验证通过,则提取xml中的加密消息
    4. 91 | *
    5. 对消息进行解密
    6. 92 | *
    93 | * 94 | * @param $msgSignature string 签名串,对应URL参数的msg_signature 95 | * @param $timestamp string 时间戳 对应URL参数的timestamp 96 | * @param $nonce string 随机串,对应URL参数的nonce 97 | * @param $postData string 密文,对应POST请求的数据 98 | * @param &$msg string 解密后的原文,当return返回0时有效 99 | * 100 | * @return int 成功0,失败返回对应的错误码 101 | */ 102 | public function decryptMsg($msgSignature, $timestamp = null, $nonce, $postData, &$msg) 103 | { 104 | if (strlen($this->encodingAesKey) != 43) { 105 | return ErrorCode::$IllegalAesKey; 106 | } 107 | 108 | $pc = new Prpcrypt($this->encodingAesKey); 109 | 110 | //提取密文 111 | $xmlparse = new XMLParse; 112 | $array = $xmlparse->extract($postData); 113 | $ret = $array[0]; 114 | 115 | if ($ret != 0) { 116 | return $ret; 117 | } 118 | 119 | if ($timestamp == null) { 120 | $timestamp = time(); 121 | } 122 | 123 | $encrypt = $array[1]; 124 | $touser_name = $array[2]; 125 | 126 | //验证安全签名 127 | $sha1 = new SHA1; 128 | $array = $sha1->getSHA1($this->token, $timestamp, $nonce, $encrypt); 129 | $ret = $array[0]; 130 | 131 | if ($ret != 0) { 132 | return $ret; 133 | } 134 | 135 | $signature = $array[1]; 136 | if ($signature != $msgSignature) { 137 | return ErrorCode::$ValidateSignatureError; 138 | } 139 | 140 | $result = $pc->decrypt($encrypt, $this->appId); 141 | if ($result[0] != 0) { 142 | return $result[0]; 143 | } 144 | $msg = $result[1]; 145 | 146 | return ErrorCode::$OK; 147 | } 148 | 149 | } 150 | 151 | -------------------------------------------------------------------------------- /src/xmlparse.php: -------------------------------------------------------------------------------- 1 | loadXML($xmltext); 22 | $array_e = $xml->getElementsByTagName('Encrypt'); 23 | $array_a = $xml->getElementsByTagName('ToUserName'); 24 | $encrypt = $array_e->item(0)->nodeValue; 25 | $tousername = $array_a->item(0)->nodeValue; 26 | return array(0, $encrypt, $tousername); 27 | } catch (Exception $e) { 28 | //print $e . "\n"; 29 | return array(ErrorCode::$ParseXmlError, null, null); 30 | } 31 | } 32 | 33 | /** 34 | * 生成xml消息 35 | * @param string $encrypt 加密后的消息密文 36 | * @param string $signature 安全签名 37 | * @param string $timestamp 时间戳 38 | * @param string $nonce 随机字符串 39 | */ 40 | public function generate($encrypt, $signature, $timestamp, $nonce) 41 | { 42 | $format = " 43 | 44 | 45 | %s 46 | 47 | "; 48 | return sprintf($format, $encrypt, $signature, $timestamp, $nonce); 49 | } 50 | 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /test/EventTest.php: -------------------------------------------------------------------------------- 1 | , NetPuter 6 | * @license MIT License 7 | */ 8 | 9 | require_once __DIR__ . '/SdkTestBase.php'; 10 | 11 | /** 12 | * Event Test 13 | */ 14 | class WechatSdkEventTest extends WechatSdkTestBase { 15 | protected $mockBuilder; 16 | 17 | protected function setUp() { 18 | parent::setUp(); 19 | 20 | $this->mockBuilder = $this->getMockBuilder('MyWechat') 21 | ->setMethods(array('onSubscribe', 'onUnsubscribe', 'onText', 'onImage', 'onLocation', 'onLink', 'onUnknown')) 22 | ->setConstructorArgs(array($this->token)); 23 | } 24 | 25 | public function testGeneralFields() { 26 | ExitTestHelper::init(); 27 | 28 | $this->fillTextMsg('填充消息'); 29 | $wechat = $this->mockBuilder->getMock(); 30 | 31 | // 无需执行run(), 所有字段应已解析完毕 32 | $this->assertEquals($this->toUser, $wechat->publicGetRequest('tousername')); 33 | $this->assertEquals($this->fromUser, $wechat->publicGetRequest('fromusername')); 34 | $this->assertEquals($this->time, $wechat->publicGetRequest('createtime')); 35 | $this->assertEquals($this->msgid, $wechat->publicGetRequest('msgid')); 36 | 37 | // 应无exit 38 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 39 | ExitTestHelper::clean(); 40 | 41 | } 42 | 43 | public function testEventOnSubscribe() { 44 | ExitTestHelper::init(); 45 | 46 | $this->fillEvent('subscribe'); 47 | $wechat = $this->mockBuilder->getMock(); 48 | $wechat->expects($this->once()) 49 | ->method('onSubscribe'); 50 | 51 | $wechat->run(); 52 | 53 | $this->assertEquals('', $wechat->publicGetRequest('eventkey')); 54 | 55 | // 应无exit 56 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 57 | 58 | ExitTestHelper::clean(); 59 | } 60 | 61 | public function testEventOnUnsubscribe() { 62 | ExitTestHelper::init(); 63 | 64 | $this->fillEvent('unsubscribe'); 65 | $wechat = $this->mockBuilder->getMock(); 66 | $wechat->expects($this->once()) 67 | ->method('onUnsubscribe'); 68 | 69 | $wechat->run(); 70 | 71 | $this->assertEquals('', $wechat->publicGetRequest('eventkey')); 72 | 73 | // 应无exit 74 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 75 | 76 | ExitTestHelper::clean(); 77 | } 78 | 79 | public function testEventOnUnknown() { 80 | ExitTestHelper::init(); 81 | 82 | $this->fillUnknown('unknown info'); 83 | $wechat = $this->mockBuilder->getMock(); 84 | $wechat->expects($this->once()) 85 | ->method('onUnknown'); 86 | 87 | $wechat->run(); 88 | 89 | $this->assertEquals('unknown info', $wechat->publicGetRequest('unknown')); 90 | 91 | // 应无exit 92 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 93 | 94 | ExitTestHelper::clean(); 95 | } 96 | 97 | public function testEventOnText() { 98 | ExitTestHelper::init(); 99 | 100 | $this->fillTextMsg('填充文本消息'); 101 | $wechat = $this->mockBuilder->getMock(); 102 | $wechat->expects($this->once()) 103 | ->method('onText'); 104 | 105 | $wechat->run(); 106 | 107 | $this->assertEquals('填充文本消息', $wechat->publicGetRequest('content')); 108 | 109 | // 应无exit 110 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 111 | 112 | ExitTestHelper::clean(); 113 | } 114 | 115 | public function testEventOnImage() { 116 | ExitTestHelper::init(); 117 | 118 | $this->fillImageMsg('https://travis-ci.org/netputer/wechat-php-sdk.png'); 119 | $wechat = $this->mockBuilder->getMock(); 120 | $wechat->expects($this->once()) 121 | ->method('onImage'); 122 | 123 | $wechat->run(); 124 | 125 | $this->assertEquals('https://travis-ci.org/netputer/wechat-php-sdk.png', $wechat->publicGetRequest('picurl')); 126 | 127 | // 应无exit 128 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 129 | 130 | ExitTestHelper::clean(); 131 | } 132 | 133 | public function testEventOnLocation() { 134 | ExitTestHelper::init(); 135 | 136 | $this->fillLocationMsg('23.134521', '113.358803'); 137 | $wechat = $this->mockBuilder->getMock(); 138 | $wechat->expects($this->once()) 139 | ->method('onLocation'); 140 | 141 | $wechat->run(); 142 | 143 | $this->assertEquals('23.134521', $wechat->publicGetRequest('location_x')); 144 | $this->assertEquals('113.358803', $wechat->publicGetRequest('location_y')); 145 | 146 | // 应无exit 147 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 148 | 149 | ExitTestHelper::clean(); 150 | } 151 | 152 | public function testEventOnLink() { 153 | ExitTestHelper::init(); 154 | 155 | $this->fillLinkMsg('netputer/wechat-php-sdk', '微信公众平台 PHP SDK', 'https://github.com/netputer/wechat-php-sdk'); 156 | $wechat = $this->mockBuilder->getMock(); 157 | $wechat->expects($this->once()) 158 | ->method('onLink'); 159 | 160 | $wechat->run(); 161 | 162 | $this->assertEquals('netputer/wechat-php-sdk', $wechat->publicGetRequest('title')); 163 | $this->assertEquals('微信公众平台 PHP SDK', $wechat->publicGetRequest('description')); 164 | $this->assertEquals('https://github.com/netputer/wechat-php-sdk', $wechat->publicGetRequest('url')); 165 | 166 | // 应无exit 167 | $this->assertFalse(ExitTestHelper::isThereExit(), "There shouldn't be any exit() was invoked."); 168 | 169 | ExitTestHelper::clean(); 170 | } 171 | 172 | } 173 | ?> -------------------------------------------------------------------------------- /test/ExitTestHelper.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright Ian Li , All rights reserved. 7 | * @link https://github.com/techotaku/TestHelpers.php 8 | * @license MIT License 9 | */ 10 | 11 | /** 12 | * PHP exit() test helper static class 13 | */ 14 | class ExitTestHelper { 15 | private static $have_exit; 16 | private static $first_exit_output; 17 | 18 | /** 19 | * Initialize. 20 | * Register exit handler. 21 | */ 22 | public static function init() { 23 | self::$have_exit = FALSE; 24 | self::$first_exit_output = NULL; 25 | set_exit_overload('ExitTestHelper::exitHandler'); 26 | ob_start(); 27 | } 28 | 29 | /** 30 | * Clean up. 31 | * Unregister exit handler. 32 | */ 33 | public static function clean() { 34 | ob_end_clean(); 35 | unset_exit_overload(); 36 | self::$have_exit = FALSE; 37 | self::$first_exit_output = NULL; 38 | } 39 | 40 | /** 41 | * Returns a value indicating whether there is any exit() was invoked. 42 | * 43 | * @return boolean 44 | */ 45 | public static function isThereExit() { 46 | return self::$have_exit; 47 | } 48 | 49 | /** 50 | * Returns all output after first exit() was invoked. 51 | * If no exit() was invoked, returns all contents in output buffer. 52 | * (This behavior will help you to remove 'ugly' exit() invoking, from your product code.) 53 | * 54 | * @return string 55 | */ 56 | public static function getFirstExitOutput() { 57 | if (!is_null(self::$first_exit_output)) 58 | { 59 | return self::$first_exit_output; 60 | } else { 61 | return ob_get_contents(); 62 | } 63 | } 64 | 65 | /** 66 | * Php exit() handler. 67 | * Make it private, to prevent invoking outside. 68 | * 69 | * @param string $param (Optional) Exit message. 70 | */ 71 | private static function exitHandler($param = NULL) { 72 | if (!(self::$have_exit)) { 73 | self::$have_exit = TRUE; 74 | echo $param ?: ''; 75 | self::$first_exit_output = ob_get_contents(); 76 | if (self::$first_exit_output === FALSE) { 77 | self::$first_exit_output = ''; 78 | } 79 | } 80 | return FALSE; 81 | } 82 | 83 | } 84 | ?> -------------------------------------------------------------------------------- /test/GeneralTest.php: -------------------------------------------------------------------------------- 1 | , NetPuter 6 | * @license MIT License 7 | */ 8 | 9 | require_once __DIR__ . '/SdkTestBase.php'; 10 | 11 | /** 12 | * General Test 13 | */ 14 | class WechatSdkGeneralTest extends WechatSdkTestBase { 15 | 16 | protected function setUp() { 17 | parent::setUp(); 18 | } 19 | 20 | public function testApiValidation() { 21 | ExitTestHelper::init(); 22 | 23 | $echostr = '9eabb7918cbad53305f7eae647cf1402e2fc7836'; 24 | $_GET['echostr'] = $echostr; 25 | 26 | $wechat = new MyWechat($this->token); 27 | $this->assertEquals($echostr, ExitTestHelper::getFirstExitOutput(), 'Wechat API validation output should match the input.'); 28 | 29 | ExitTestHelper::clean(); 30 | } 31 | 32 | public function testBlankSignature() { 33 | ExitTestHelper::init(); 34 | 35 | $_GET['signature'] = ''; 36 | $wechat = new MyWechat($this->token); 37 | $this->assertEquals('签名验证失败', ExitTestHelper::getFirstExitOutput(), 'Signature verification should fail.'); 38 | 39 | ExitTestHelper::clean(); 40 | } 41 | 42 | public function testEmptyPOST() { 43 | ExitTestHelper::init(); 44 | 45 | $wechat = new MyWechat($this->token); 46 | $this->assertEquals('缺少数据', ExitTestHelper::getFirstExitOutput(), 'SDK should output "no data" (in chinese, utf-8).'); 47 | 48 | ExitTestHelper::clean(); 49 | } 50 | 51 | } 52 | ?> -------------------------------------------------------------------------------- /test/MyWechat.php: -------------------------------------------------------------------------------- 1 | , NetPuter 6 | * @license MIT License 7 | */ 8 | 9 | /** 10 | * 测试对象 11 | */ 12 | class MyWechat extends Wechat { 13 | 14 | /** 15 | * 方法getRequest的测试钩子 16 | */ 17 | public function publicGetRequest($param = FALSE) { 18 | return parent::getRequest($param); 19 | } 20 | 21 | /** 22 | * 方法responseText的测试钩子 23 | */ 24 | public function publicResponseText($content, $funcFlag = 0) { 25 | parent::responseText($content, $funcFlag); 26 | } 27 | 28 | /** 29 | * 方法responseMusic的测试钩子 30 | */ 31 | public function publicResponseMusic($title, $description, $musicUrl, $hqMusicUrl, $funcFlag = 0) { 32 | parent::responseMusic($title, $description, $musicUrl, $hqMusicUrl, $funcFlag); 33 | } 34 | 35 | /** 36 | * 方法responseNews的测试钩子 37 | */ 38 | public function publicResponseNews($items, $funcFlag = 0) { 39 | parent::responseNews($items, $funcFlag); 40 | } 41 | 42 | 43 | 44 | /** 45 | * 用户关注时触发,已mock 46 | * 47 | * @return void 48 | */ 49 | protected function onSubscribe() { 50 | // 已mock 51 | } 52 | 53 | /** 54 | * 用户取消关注时触发,已mock 55 | * 56 | * @return void 57 | */ 58 | protected function onUnsubscribe() { 59 | // 已mock 60 | } 61 | 62 | /** 63 | * 收到文本消息时触发,已mock 64 | * 65 | * @return void 66 | */ 67 | protected function onText() { 68 | // 已mock 69 | } 70 | 71 | /** 72 | * 收到图片消息时触发,已mock 73 | * 74 | * @return void 75 | */ 76 | protected function onImage() { 77 | // 已mock 78 | } 79 | 80 | /** 81 | * 收到地理位置消息时触发,已mock 82 | * 83 | * @return void 84 | */ 85 | protected function onLocation() { 86 | // 已mock 87 | } 88 | 89 | /** 90 | * 收到链接消息时触发,已mock 91 | * 92 | * @return void 93 | */ 94 | protected function onLink() { 95 | // 已mock 96 | } 97 | 98 | /** 99 | * 收到未知类型消息时触发,已mock 100 | * 101 | * @return void 102 | */ 103 | protected function onUnknown() { 104 | // 已mock 105 | } 106 | 107 | } 108 | ?> -------------------------------------------------------------------------------- /test/ReplyTest.php: -------------------------------------------------------------------------------- 1 | , NetPuter 6 | * @license MIT License 7 | */ 8 | 9 | require_once __DIR__ . '/SdkTestBase.php'; 10 | 11 | /** 12 | * Reply Test 13 | */ 14 | class WechatSdkReplyTest extends WechatSdkTestBase { 15 | protected $response; 16 | 17 | protected function setUp() { 18 | parent::setUp(); 19 | 20 | $this->response = NULL; 21 | } 22 | 23 | private function setResponse($response) { 24 | $xml = (array) simplexml_load_string($response, 'SimpleXMLElement', LIBXML_NOCDATA); 25 | $this->response = array_change_key_case($xml, CASE_LOWER); 26 | } 27 | 28 | private function getResponseField($key) { 29 | if (isset($this->response[$key])) { 30 | return $this->response[$key]; 31 | } else { 32 | return NULL; 33 | } 34 | } 35 | 36 | public function testReplyText() { 37 | ExitTestHelper::init(); 38 | 39 | $this->fillTextMsg('收到文本消息'); 40 | $wechat = new MyWechat($this->token); 41 | 42 | // 发出回复 43 | $wechat->publicResponseText('回复文本消息'); 44 | 45 | // 截获输出,解析 46 | $this->setResponse(ExitTestHelper::getFirstExitOutput()); 47 | 48 | // 回复的to、from与填充的传入消息的to、from相反 49 | $this->assertEquals($this->fromUser, $this->getResponseField('tousername')); 50 | $this->assertEquals($this->toUser, $this->getResponseField('fromusername')); 51 | $this->assertEquals('0', $this->getResponseField('funcflag')); 52 | $this->assertEquals('text', $this->getResponseField('msgtype')); 53 | 54 | // 验证文本回复内容 55 | $this->assertEquals('回复文本消息', $this->getResponseField('content')); 56 | 57 | ExitTestHelper::clean(); 58 | } 59 | 60 | public function testReplyMusic() { 61 | ExitTestHelper::init(); 62 | 63 | $this->fillTextMsg('收到文本消息'); 64 | $wechat = new MyWechat($this->token); 65 | 66 | // 回复音乐消息 67 | $wechat->publicResponseMusic('音乐标题', 68 | '音乐说明', 69 | 'http://sample.net/music.mp3', 70 | 'http://sample.net/hqmusic.mp3'); 71 | 72 | // 截获输出,解析 73 | $this->setResponse(ExitTestHelper::getFirstExitOutput()); 74 | 75 | // 回复的to、from与填充的传入消息的to、from相反 76 | $this->assertEquals($this->fromUser, $this->getResponseField('tousername')); 77 | $this->assertEquals($this->toUser, $this->getResponseField('fromusername')); 78 | $this->assertEquals('0', $this->getResponseField('funcflag')); 79 | $this->assertEquals('music', $this->getResponseField('msgtype')); 80 | 81 | // 验证音乐消息内容 82 | $music = array_change_key_case((array) $this->getResponseField('music'), CASE_LOWER); 83 | $this->assertEquals(array( 84 | 'title' => '音乐标题', 85 | 'description' => '音乐说明', 86 | 'musicurl' => 'http://sample.net/music.mp3', 87 | 'hqmusicurl' => 'http://sample.net/hqmusic.mp3'), 88 | $music); 89 | 90 | ExitTestHelper::clean(); 91 | } 92 | 93 | public function testReplyNews() { 94 | ExitTestHelper::init(); 95 | 96 | $this->fillTextMsg('收到文本消息'); 97 | $wechat = new MyWechat($this->token); 98 | 99 | $items = array( 100 | new NewsResponseItem('Travis CI', 101 | 'Free Hosted Continuous Integration Platform for the Open Source Community', 102 | 'https://travis-ci.org/netputer/wechat-php-sdk.png', 103 | 'https://travis-ci.org/netputer/wechat-php-sdk'), 104 | new NewsResponseItem('Travis CI 2', 105 | '2 Free Hosted Continuous Integration Platform for the Open Source Community', 106 | 'https://travis-ci.org/netputer/wechat-php-sdk.png', 107 | 'https://travis-ci.org/netputer/wechat-php-sdk') 108 | ); 109 | 110 | // 回复图文消息 111 | $wechat->publicResponseNews($items); 112 | 113 | // 截获输出,解析 114 | $this->setResponse(ExitTestHelper::getFirstExitOutput()); 115 | 116 | // 回复的to、from与填充的传入消息的to、from相反 117 | $this->assertEquals($this->fromUser, $this->getResponseField('tousername')); 118 | $this->assertEquals($this->toUser, $this->getResponseField('fromusername')); 119 | $this->assertEquals('0', $this->getResponseField('funcflag')); 120 | $this->assertEquals('news', $this->getResponseField('msgtype')); 121 | 122 | // 验证图文消息内容 123 | $this->assertEquals('2', $this->getResponseField('articlecount')); 124 | 125 | $articles = (array) $this->getResponseField('articles'); 126 | $article = array_change_key_case((array) $articles['item'][0], CASE_LOWER); 127 | $this->assertEquals(array( 128 | 'title' => 'Travis CI', 129 | 'description' => 'Free Hosted Continuous Integration Platform for the Open Source Community', 130 | 'picurl' => 'https://travis-ci.org/netputer/wechat-php-sdk.png', 131 | 'url' => 'https://travis-ci.org/netputer/wechat-php-sdk'), 132 | $article); 133 | $article = array_change_key_case((array) $articles['item'][1], CASE_LOWER); 134 | $this->assertEquals(array( 135 | 'title' => 'Travis CI 2', 136 | 'description' => '2 Free Hosted Continuous Integration Platform for the Open Source Community', 137 | 'picurl' => 'https://travis-ci.org/netputer/wechat-php-sdk.png', 138 | 'url' => 'https://travis-ci.org/netputer/wechat-php-sdk'), 139 | $article); 140 | 141 | ExitTestHelper::clean(); 142 | } 143 | 144 | 145 | } 146 | ?> -------------------------------------------------------------------------------- /test/SdkTestBase.php: -------------------------------------------------------------------------------- 1 | , NetPuter 6 | * @license MIT License 7 | */ 8 | 9 | require __DIR__ . '/../src/Wechat.php'; 10 | require __DIR__ . '/ExitTestHelper.php'; 11 | require __DIR__ . '/MyWechat.php'; 12 | 13 | /** 14 | * Test Base 15 | */ 16 | class WechatSdkTestBase extends PHPUnit_Framework_TestCase { 17 | protected $token; 18 | protected $signature; 19 | protected $toUser; 20 | protected $fromUser; 21 | protected $time; 22 | protected $msgid; 23 | 24 | protected function setUp() { 25 | $this->token = 'wechat-php-sdk'; 26 | 27 | $_GET['timestamp'] = time(); 28 | $_GET['nonce'] = rand(10000000, 99999999); 29 | $signatureArray = array($this->token, $_GET['timestamp'], $_GET['nonce']); 30 | sort($signatureArray); 31 | $_GET['signature'] = sha1(implode($signatureArray)); 32 | 33 | $this->signature = $_GET['signature']; 34 | 35 | $this->toUser = 'mp.weixin'; 36 | $this->fromUser = 'fromUser'; 37 | $this->time = time(); 38 | $this->msgid = '1234567890123456'; 39 | } 40 | 41 | protected function fillTextMsg($param) { 42 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 43 | toUser]]> 44 | fromUser]]> 45 | $this->time 46 | 47 | 48 | $this->msgid 49 | "; 50 | } 51 | 52 | protected function fillImageMsg($param) { 53 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 54 | toUser]]> 55 | fromUser]]> 56 | $this->time 57 | 58 | 59 | $this->msgid 60 | "; 61 | } 62 | 63 | protected function fillLocationMsg($x, $y) { 64 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 65 | toUser]]> 66 | fromUser]]> 67 | $this->time 68 | 69 | $x 70 | $y 71 | 20 72 | 73 | $this->msgid 74 | "; 75 | } 76 | 77 | protected function fillLinkMsg($title, $description, $url) { 78 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 79 | toUser]]> 80 | fromUser]]> 81 | $this->time 82 | 83 | <![CDATA[$title]]> 84 | 85 | 86 | $this->msgid 87 | "; 88 | } 89 | 90 | protected function fillUnknown($param) { 91 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 92 | toUser]]> 93 | fromUser]]> 94 | $this->time 95 | 96 | 97 | $this->msgid 98 | "; 99 | } 100 | 101 | protected function fillEvent($event, $eventKey = '') { 102 | $GLOBALS['HTTP_RAW_POST_DATA'] = " 103 | toUser]]> 104 | fromUser]]> 105 | $this->time 106 | 107 | 108 | 109 | $this->msgid 110 | "; 111 | } 112 | 113 | } 114 | ?> -------------------------------------------------------------------------------- /test/install-extensions.sh: -------------------------------------------------------------------------------- 1 | git clone --depth=1 --branch=master git://github.com/php-test-helpers/php-test-helpers.git php-test-helpers 2 | cd php-test-helpers 3 | phpize 4 | ./configure --enable-test-helpers 5 | make 6 | res=`sudo make install` 7 | echo "zend_extension =${res##*Installing shared extensions: }test_helpers.so" 8 | echo "zend_extension =${res##*Installing shared extensions: }test_helpers.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/conf.d/xdebug.ini 9 | cd .. 10 | php --version 11 | --------------------------------------------------------------------------------