├── .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 |
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 |
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 | * - 对要发送的消息进行AES-CBC加密
42 | * - 生成安全签名
43 | * - 将消息密文和安全签名打包成xml格式
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 | * - 利用收到的密文生成安全签名,进行签名验证
90 | * - 若验证通过,则提取xml中的加密消息
91 | * - 对消息进行解密
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 |
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 |
--------------------------------------------------------------------------------