Guard.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <?php
  2. /*
  3. * This file is part of the overtrue/wechat.
  4. *
  5. * (c) overtrue <i@overtrue.me>
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. /**
  11. * Guard.php.
  12. *
  13. * @author overtrue <i@overtrue.me>
  14. * @copyright 2015 overtrue <i@overtrue.me>
  15. *
  16. * @see https://github.com/overtrue
  17. * @see http://overtrue.me
  18. */
  19. namespace EasyWeChat\Server;
  20. use EasyWeChat\Core\Exceptions\FaultException;
  21. use EasyWeChat\Core\Exceptions\InvalidArgumentException;
  22. use EasyWeChat\Core\Exceptions\RuntimeException;
  23. use EasyWeChat\Encryption\Encryptor;
  24. use EasyWeChat\Message\AbstractMessage;
  25. use EasyWeChat\Message\Raw as RawMessage;
  26. use EasyWeChat\Message\Text;
  27. use EasyWeChat\Support\Collection;
  28. use EasyWeChat\Support\Log;
  29. use EasyWeChat\Support\XML;
  30. use Symfony\Component\HttpFoundation\Request;
  31. use Symfony\Component\HttpFoundation\Response;
  32. /**
  33. * Class Guard.
  34. */
  35. class Guard
  36. {
  37. /**
  38. * Empty string.
  39. */
  40. const SUCCESS_EMPTY_RESPONSE = 'success';
  41. const TEXT_MSG = 2;
  42. const IMAGE_MSG = 4;
  43. const VOICE_MSG = 8;
  44. const VIDEO_MSG = 16;
  45. const SHORT_VIDEO_MSG = 32;
  46. const LOCATION_MSG = 64;
  47. const LINK_MSG = 128;
  48. const DEVICE_EVENT_MSG = 256;
  49. const DEVICE_TEXT_MSG = 512;
  50. const FILE_MSG = 1024;
  51. const EVENT_MSG = 1048576;
  52. const ALL_MSG = 1050622;
  53. /**
  54. * @var Request
  55. */
  56. protected $request;
  57. /**
  58. * @var string
  59. */
  60. protected $token;
  61. /**
  62. * @var Encryptor
  63. */
  64. protected $encryptor;
  65. /**
  66. * @var string|callable
  67. */
  68. protected $messageHandler;
  69. /**
  70. * @var int
  71. */
  72. protected $messageFilter;
  73. /**
  74. * @var array
  75. */
  76. protected $messageTypeMapping = [
  77. 'text' => 2,
  78. 'image' => 4,
  79. 'voice' => 8,
  80. 'video' => 16,
  81. 'shortvideo' => 32,
  82. 'location' => 64,
  83. 'link' => 128,
  84. 'device_event' => 256,
  85. 'device_text' => 512,
  86. 'file' => 1024,
  87. 'event' => 1048576,
  88. ];
  89. /**
  90. * @var bool
  91. */
  92. protected $debug = false;
  93. /**
  94. * Constructor.
  95. *
  96. * @param string $token
  97. * @param Request $request
  98. */
  99. public function __construct($token, Request $request = null)
  100. {
  101. $this->token = $token;
  102. $this->request = $request ?: Request::createFromGlobals();
  103. }
  104. /**
  105. * Enable/Disable debug mode.
  106. *
  107. * @param bool $debug
  108. *
  109. * @return $this
  110. */
  111. public function debug($debug = true)
  112. {
  113. $this->debug = $debug;
  114. return $this;
  115. }
  116. /**
  117. * Handle and return response.
  118. *
  119. * @return Response
  120. *
  121. * @throws BadRequestException
  122. */
  123. public function serve()
  124. {
  125. Log::debug('Request received:', [
  126. 'Method' => $this->request->getMethod(),
  127. 'URI' => $this->request->getRequestUri(),
  128. 'Query' => $this->request->getQueryString(),
  129. 'Protocal' => $this->request->server->get('SERVER_PROTOCOL'),
  130. 'Content' => $this->request->getContent(),
  131. ]);
  132. $this->validate($this->token);
  133. if ($str = $this->request->get('echostr')) {
  134. Log::debug("Output 'echostr' is '$str'.");
  135. @file_put_contents('quanju3.txt',json_encode($str)."-测试1\r\n",8);
  136. return new Response($str);
  137. }
  138. $result = $this->handleRequest();
  139. @file_put_contents('quanju3.txt',json_encode($result)."-测试2\r\n",8);
  140. $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
  141. @file_put_contents('quanju3.txt',json_encode($response)."-测试3\r\n",8);
  142. Log::debug('Server response created:', compact('response'));
  143. return new Response($response);
  144. }
  145. /**
  146. * Validation request params.
  147. *
  148. * @param string $token
  149. *
  150. * @throws FaultException
  151. */
  152. public function validate($token)
  153. {
  154. $params = [
  155. $token,
  156. $this->request->get('timestamp'),
  157. $this->request->get('nonce'),
  158. ];
  159. if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
  160. throw new FaultException('Invalid request signature.', 400);
  161. }
  162. }
  163. /**
  164. * Add a event listener.
  165. *
  166. * @param callable $callback
  167. * @param int $option
  168. *
  169. * @return Guard
  170. *
  171. * @throws InvalidArgumentException
  172. */
  173. public function setMessageHandler($callback = null, $option = self::ALL_MSG)
  174. {
  175. if (!is_callable($callback)) {
  176. throw new InvalidArgumentException('Argument #2 is not callable.');
  177. }
  178. $this->messageHandler = $callback;
  179. $this->messageFilter = $option;
  180. return $this;
  181. }
  182. /**
  183. * Return the message listener.
  184. *
  185. * @return string
  186. */
  187. public function getMessageHandler()
  188. {
  189. return $this->messageHandler;
  190. }
  191. /**
  192. * Request getter.
  193. *
  194. * @return Request
  195. */
  196. public function getRequest()
  197. {
  198. return $this->request;
  199. }
  200. /**
  201. * Request setter.
  202. *
  203. * @param Request $request
  204. *
  205. * @return $this
  206. */
  207. public function setRequest(Request $request)
  208. {
  209. $this->request = $request;
  210. return $this;
  211. }
  212. /**
  213. * Set Encryptor.
  214. *
  215. * @param Encryptor $encryptor
  216. *
  217. * @return Guard
  218. */
  219. public function setEncryptor(Encryptor $encryptor)
  220. {
  221. $this->encryptor = $encryptor;
  222. return $this;
  223. }
  224. /**
  225. * Return the encryptor instance.
  226. *
  227. * @return Encryptor
  228. */
  229. public function getEncryptor()
  230. {
  231. return $this->encryptor;
  232. }
  233. /**
  234. * Build response.
  235. *
  236. * @param $to
  237. * @param $from
  238. * @param mixed $message
  239. *
  240. * @return string
  241. *
  242. * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
  243. */
  244. protected function buildResponse($to, $from, $message)
  245. {
  246. if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
  247. return self::SUCCESS_EMPTY_RESPONSE;
  248. }
  249. if ($message instanceof RawMessage) {
  250. return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
  251. }
  252. if (is_string($message) || is_numeric($message)) {
  253. $message = new Text(['content' => $message]);
  254. }
  255. if (!$this->isMessage($message)) {
  256. $messageType = gettype($message);
  257. throw new InvalidArgumentException("Invalid Message type .'{$messageType}'");
  258. }
  259. $response = $this->buildReply($to, $from, $message);
  260. if ($this->isSafeMode()) {
  261. Log::debug('Message safe mode is enable.');
  262. $response = $this->encryptor->encryptMsg(
  263. $response,
  264. $this->request->get('nonce'),
  265. $this->request->get('timestamp')
  266. );
  267. }
  268. return $response;
  269. }
  270. /**
  271. * Whether response is message.
  272. *
  273. * @param mixed $message
  274. *
  275. * @return bool
  276. */
  277. protected function isMessage($message)
  278. {
  279. if (is_array($message)) {
  280. foreach ($message as $element) {
  281. if (!is_subclass_of($element, AbstractMessage::class)) {
  282. return false;
  283. }
  284. }
  285. return true;
  286. }
  287. return is_subclass_of($message, AbstractMessage::class);
  288. }
  289. /**
  290. * Get request message.
  291. *
  292. * @return array
  293. *
  294. * @throws BadRequestException
  295. */
  296. public function getMessage()
  297. {
  298. $message = $this->parseMessageFromRequest($this->request->getContent(false));
  299. if (!is_array($message) || empty($message)) {
  300. throw new BadRequestException('Invalid request.');
  301. }
  302. return $message;
  303. }
  304. /**
  305. * Handle request.
  306. *
  307. * @return array
  308. *
  309. * @throws \EasyWeChat\Core\Exceptions\RuntimeException
  310. * @throws \EasyWeChat\Server\BadRequestException
  311. */
  312. protected function handleRequest()
  313. {
  314. $message = $this->getMessage();
  315. $response = $this->handleMessage($message);
  316. $messageType = isset($message['msg_type']) ? $message['msg_type'] : $message['MsgType'];
  317. if ('device_text' === $messageType) {
  318. $message['FromUserName'] = '';
  319. $message['ToUserName'] = '';
  320. }
  321. return [
  322. 'to' => $message['FromUserName'],
  323. 'from' => $message['ToUserName'],
  324. 'response' => $response,
  325. ];
  326. }
  327. /**
  328. * Handle message.
  329. *
  330. * @param array $message
  331. *
  332. * @return mixed
  333. */
  334. protected function handleMessage(array $message)
  335. {
  336. $handler = $this->messageHandler;
  337. if (!is_callable($handler)) {
  338. Log::debug('No handler enabled.');
  339. return null;
  340. }
  341. Log::debug('Message detail:', $message);
  342. $message = new Collection($message);
  343. $messageType = $message->get('msg_type', $message->get('MsgType'));
  344. $type = $this->messageTypeMapping[$messageType];
  345. $response = null;
  346. if ($this->messageFilter & $type) {
  347. $response = call_user_func_array($handler, [$message]);
  348. }
  349. return $response;
  350. }
  351. /**
  352. * Build reply XML.
  353. *
  354. * @param string $to
  355. * @param string $from
  356. * @param AbstractMessage $message
  357. *
  358. * @return string
  359. */
  360. protected function buildReply($to, $from, $message)
  361. {
  362. $base = [
  363. 'ToUserName' => $to,
  364. 'FromUserName' => $from,
  365. 'CreateTime' => time(),
  366. 'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
  367. ];
  368. $transformer = new Transformer();
  369. return XML::build(array_merge($base, $transformer->transform($message)));
  370. }
  371. /**
  372. * Get signature.
  373. *
  374. * @param array $request
  375. *
  376. * @return string
  377. */
  378. protected function signature($request)
  379. {
  380. sort($request, SORT_STRING);
  381. return sha1(implode($request));
  382. }
  383. /**
  384. * Parse message array from raw php input.
  385. *
  386. * @param string|resource $content
  387. *
  388. * @throws \EasyWeChat\Core\Exceptions\RuntimeException
  389. * @throws \EasyWeChat\Encryption\EncryptionException
  390. *
  391. * @return array
  392. */
  393. protected function parseMessageFromRequest($content)
  394. {
  395. $content = strval($content);
  396. $dataSet = json_decode($content, true);
  397. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  398. // For mini-program JSON formats.
  399. // Convert to XML if the given string can be decode into a data array.
  400. $content = XML::build($dataSet);
  401. }
  402. if ($this->isSafeMode()) {
  403. if (!$this->encryptor) {
  404. throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
  405. }
  406. $message = $this->encryptor->decryptMsg(
  407. $this->request->get('msg_signature'),
  408. $this->request->get('nonce'),
  409. $this->request->get('timestamp'),
  410. $content
  411. );
  412. } else {
  413. $message = XML::parse($content);
  414. }
  415. return $message;
  416. }
  417. /**
  418. * Check the request message safe mode.
  419. *
  420. * @return bool
  421. */
  422. private function isSafeMode()
  423. {
  424. return $this->request->get('encrypt_type') && 'aes' === $this->request->get('encrypt_type');
  425. }
  426. }