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

499 lines
11 KiB

  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. return new Response($str);
  136. }
  137. $result = $this->handleRequest();
  138. $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
  139. Log::debug('Server response created:', compact('response'));
  140. return new Response($response);
  141. }
  142. /**
  143. * Validation request params.
  144. *
  145. * @param string $token
  146. *
  147. * @throws FaultException
  148. */
  149. public function validate($token)
  150. {
  151. $params = [
  152. $token,
  153. $this->request->get('timestamp'),
  154. $this->request->get('nonce'),
  155. ];
  156. if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
  157. throw new FaultException('Invalid request signature.', 400);
  158. }
  159. }
  160. /**
  161. * Add a event listener.
  162. *
  163. * @param callable $callback
  164. * @param int $option
  165. *
  166. * @return Guard
  167. *
  168. * @throws InvalidArgumentException
  169. */
  170. public function setMessageHandler($callback = null, $option = self::ALL_MSG)
  171. {
  172. if (!is_callable($callback)) {
  173. throw new InvalidArgumentException('Argument #2 is not callable.');
  174. }
  175. $this->messageHandler = $callback;
  176. $this->messageFilter = $option;
  177. return $this;
  178. }
  179. /**
  180. * Return the message listener.
  181. *
  182. * @return string
  183. */
  184. public function getMessageHandler()
  185. {
  186. return $this->messageHandler;
  187. }
  188. /**
  189. * Request getter.
  190. *
  191. * @return Request
  192. */
  193. public function getRequest()
  194. {
  195. return $this->request;
  196. }
  197. /**
  198. * Request setter.
  199. *
  200. * @param Request $request
  201. *
  202. * @return $this
  203. */
  204. public function setRequest(Request $request)
  205. {
  206. $this->request = $request;
  207. return $this;
  208. }
  209. /**
  210. * Set Encryptor.
  211. *
  212. * @param Encryptor $encryptor
  213. *
  214. * @return Guard
  215. */
  216. public function setEncryptor(Encryptor $encryptor)
  217. {
  218. $this->encryptor = $encryptor;
  219. return $this;
  220. }
  221. /**
  222. * Return the encryptor instance.
  223. *
  224. * @return Encryptor
  225. */
  226. public function getEncryptor()
  227. {
  228. return $this->encryptor;
  229. }
  230. /**
  231. * Build response.
  232. *
  233. * @param $to
  234. * @param $from
  235. * @param mixed $message
  236. *
  237. * @return string
  238. *
  239. * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
  240. */
  241. protected function buildResponse($to, $from, $message)
  242. {
  243. if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
  244. return self::SUCCESS_EMPTY_RESPONSE;
  245. }
  246. if ($message instanceof RawMessage) {
  247. return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
  248. }
  249. if (is_string($message) || is_numeric($message)) {
  250. $message = new Text(['content' => $message]);
  251. }
  252. if (!$this->isMessage($message)) {
  253. $messageType = gettype($message);
  254. throw new InvalidArgumentException("Invalid Message type .'{$messageType}'");
  255. }
  256. $response = $this->buildReply($to, $from, $message);
  257. if ($this->isSafeMode()) {
  258. Log::debug('Message safe mode is enable.');
  259. $response = $this->encryptor->encryptMsg(
  260. $response,
  261. $this->request->get('nonce'),
  262. $this->request->get('timestamp')
  263. );
  264. }
  265. return $response;
  266. }
  267. /**
  268. * Whether response is message.
  269. *
  270. * @param mixed $message
  271. *
  272. * @return bool
  273. */
  274. protected function isMessage($message)
  275. {
  276. if (is_array($message)) {
  277. foreach ($message as $element) {
  278. if (!is_subclass_of($element, AbstractMessage::class)) {
  279. return false;
  280. }
  281. }
  282. return true;
  283. }
  284. return is_subclass_of($message, AbstractMessage::class);
  285. }
  286. /**
  287. * Get request message.
  288. *
  289. * @return array
  290. *
  291. * @throws BadRequestException
  292. */
  293. public function getMessage()
  294. {
  295. $message = $this->parseMessageFromRequest($this->request->getContent(false));
  296. if (!is_array($message) || empty($message)) {
  297. throw new BadRequestException('Invalid request.');
  298. }
  299. return $message;
  300. }
  301. /**
  302. * Handle request.
  303. *
  304. * @return array
  305. *
  306. * @throws \EasyWeChat\Core\Exceptions\RuntimeException
  307. * @throws \EasyWeChat\Server\BadRequestException
  308. */
  309. protected function handleRequest()
  310. {
  311. $message = $this->getMessage();
  312. $response = $this->handleMessage($message);
  313. $messageType = isset($message['msg_type']) ? $message['msg_type'] : $message['MsgType'];
  314. if ('device_text' === $messageType) {
  315. $message['FromUserName'] = '';
  316. $message['ToUserName'] = '';
  317. }
  318. return [
  319. 'to' => $message['FromUserName'],
  320. 'from' => $message['ToUserName'],
  321. 'response' => $response,
  322. ];
  323. }
  324. /**
  325. * Handle message.
  326. *
  327. * @param array $message
  328. *
  329. * @return mixed
  330. */
  331. protected function handleMessage(array $message)
  332. {
  333. $handler = $this->messageHandler;
  334. if (!is_callable($handler)) {
  335. Log::debug('No handler enabled.');
  336. return null;
  337. }
  338. Log::debug('Message detail:', $message);
  339. $message = new Collection($message);
  340. $messageType = $message->get('msg_type', $message->get('MsgType'));
  341. $type = $this->messageTypeMapping[$messageType];
  342. $response = null;
  343. if ($this->messageFilter & $type) {
  344. $response = call_user_func_array($handler, [$message]);
  345. }
  346. return $response;
  347. }
  348. /**
  349. * Build reply XML.
  350. *
  351. * @param string $to
  352. * @param string $from
  353. * @param AbstractMessage $message
  354. *
  355. * @return string
  356. */
  357. protected function buildReply($to, $from, $message)
  358. {
  359. $base = [
  360. 'ToUserName' => $to,
  361. 'FromUserName' => $from,
  362. 'CreateTime' => time(),
  363. 'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
  364. ];
  365. $transformer = new Transformer();
  366. return XML::build(array_merge($base, $transformer->transform($message)));
  367. }
  368. /**
  369. * Get signature.
  370. *
  371. * @param array $request
  372. *
  373. * @return string
  374. */
  375. protected function signature($request)
  376. {
  377. sort($request, SORT_STRING);
  378. return sha1(implode($request));
  379. }
  380. /**
  381. * Parse message array from raw php input.
  382. *
  383. * @param string|resource $content
  384. *
  385. * @throws \EasyWeChat\Core\Exceptions\RuntimeException
  386. * @throws \EasyWeChat\Encryption\EncryptionException
  387. *
  388. * @return array
  389. */
  390. protected function parseMessageFromRequest($content)
  391. {
  392. $content = strval($content);
  393. $dataSet = json_decode($content, true);
  394. if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
  395. // For mini-program JSON formats.
  396. // Convert to XML if the given string can be decode into a data array.
  397. $content = XML::build($dataSet);
  398. }
  399. if ($this->isSafeMode()) {
  400. if (!$this->encryptor) {
  401. throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
  402. }
  403. $message = $this->encryptor->decryptMsg(
  404. $this->request->get('msg_signature'),
  405. $this->request->get('nonce'),
  406. $this->request->get('timestamp'),
  407. $content
  408. );
  409. } else {
  410. $message = XML::parse($content);
  411. }
  412. return $message;
  413. }
  414. /**
  415. * Check the request message safe mode.
  416. *
  417. * @return bool
  418. */
  419. private function isSafeMode()
  420. {
  421. return $this->request->get('encrypt_type') && 'aes' === $this->request->get('encrypt_type');
  422. }
  423. }