|
- <?php
-
- /*
- * This file is part of the overtrue/wechat.
- *
- * (c) overtrue <i@overtrue.me>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
-
- /**
- * Guard.php.
- *
- * @author overtrue <i@overtrue.me>
- * @copyright 2015 overtrue <i@overtrue.me>
- *
- * @see https://github.com/overtrue
- * @see http://overtrue.me
- */
-
- namespace EasyWeChat\Server;
-
- use EasyWeChat\Core\Exceptions\FaultException;
- use EasyWeChat\Core\Exceptions\InvalidArgumentException;
- use EasyWeChat\Core\Exceptions\RuntimeException;
- use EasyWeChat\Encryption\Encryptor;
- use EasyWeChat\Message\AbstractMessage;
- use EasyWeChat\Message\Raw as RawMessage;
- use EasyWeChat\Message\Text;
- use EasyWeChat\Support\Collection;
- use EasyWeChat\Support\Log;
- use EasyWeChat\Support\XML;
- use Symfony\Component\HttpFoundation\Request;
- use Symfony\Component\HttpFoundation\Response;
-
- /**
- * Class Guard.
- */
- class Guard
- {
- /**
- * Empty string.
- */
- const SUCCESS_EMPTY_RESPONSE = 'success';
-
- const TEXT_MSG = 2;
- const IMAGE_MSG = 4;
- const VOICE_MSG = 8;
- const VIDEO_MSG = 16;
- const SHORT_VIDEO_MSG = 32;
- const LOCATION_MSG = 64;
- const LINK_MSG = 128;
- const DEVICE_EVENT_MSG = 256;
- const DEVICE_TEXT_MSG = 512;
- const FILE_MSG = 1024;
- const EVENT_MSG = 1048576;
- const ALL_MSG = 1050622;
-
- /**
- * @var Request
- */
- protected $request;
-
- /**
- * @var string
- */
- protected $token;
-
- /**
- * @var Encryptor
- */
- protected $encryptor;
-
- /**
- * @var string|callable
- */
- protected $messageHandler;
-
- /**
- * @var int
- */
- protected $messageFilter;
-
- /**
- * @var array
- */
- protected $messageTypeMapping = [
- 'text' => 2,
- 'image' => 4,
- 'voice' => 8,
- 'video' => 16,
- 'shortvideo' => 32,
- 'location' => 64,
- 'link' => 128,
- 'device_event' => 256,
- 'device_text' => 512,
- 'file' => 1024,
- 'event' => 1048576,
- ];
-
- /**
- * @var bool
- */
- protected $debug = false;
-
- /**
- * Constructor.
- *
- * @param string $token
- * @param Request $request
- */
- public function __construct($token, Request $request = null)
- {
- $this->token = $token;
- $this->request = $request ?: Request::createFromGlobals();
- }
-
- /**
- * Enable/Disable debug mode.
- *
- * @param bool $debug
- *
- * @return $this
- */
- public function debug($debug = true)
- {
- $this->debug = $debug;
-
- return $this;
- }
-
- /**
- * Handle and return response.
- *
- * @return Response
- *
- * @throws BadRequestException
- */
- public function serve()
- {
- Log::debug('Request received:', [
- 'Method' => $this->request->getMethod(),
- 'URI' => $this->request->getRequestUri(),
- 'Query' => $this->request->getQueryString(),
- 'Protocal' => $this->request->server->get('SERVER_PROTOCOL'),
- 'Content' => $this->request->getContent(),
- ]);
-
- $this->validate($this->token);
-
- if ($str = $this->request->get('echostr')) {
- Log::debug("Output 'echostr' is '$str'.");
-
- return new Response($str);
- }
-
- $result = $this->handleRequest();
-
- $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
-
- Log::debug('Server response created:', compact('response'));
-
- return new Response($response);
- }
-
- /**
- * Validation request params.
- *
- * @param string $token
- *
- * @throws FaultException
- */
- public function validate($token)
- {
- $params = [
- $token,
- $this->request->get('timestamp'),
- $this->request->get('nonce'),
- ];
-
- if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
- throw new FaultException('Invalid request signature.', 400);
- }
- }
-
- /**
- * Add a event listener.
- *
- * @param callable $callback
- * @param int $option
- *
- * @return Guard
- *
- * @throws InvalidArgumentException
- */
- public function setMessageHandler($callback = null, $option = self::ALL_MSG)
- {
- if (!is_callable($callback)) {
- throw new InvalidArgumentException('Argument #2 is not callable.');
- }
-
- $this->messageHandler = $callback;
- $this->messageFilter = $option;
-
- return $this;
- }
-
- /**
- * Return the message listener.
- *
- * @return string
- */
- public function getMessageHandler()
- {
- return $this->messageHandler;
- }
-
- /**
- * Request getter.
- *
- * @return Request
- */
- public function getRequest()
- {
- return $this->request;
- }
-
- /**
- * Request setter.
- *
- * @param Request $request
- *
- * @return $this
- */
- public function setRequest(Request $request)
- {
- $this->request = $request;
-
- return $this;
- }
-
- /**
- * Set Encryptor.
- *
- * @param Encryptor $encryptor
- *
- * @return Guard
- */
- public function setEncryptor(Encryptor $encryptor)
- {
- $this->encryptor = $encryptor;
-
- return $this;
- }
-
- /**
- * Return the encryptor instance.
- *
- * @return Encryptor
- */
- public function getEncryptor()
- {
- return $this->encryptor;
- }
-
- /**
- * Build response.
- *
- * @param $to
- * @param $from
- * @param mixed $message
- *
- * @return string
- *
- * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
- */
- protected function buildResponse($to, $from, $message)
- {
- if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
- return self::SUCCESS_EMPTY_RESPONSE;
- }
-
- if ($message instanceof RawMessage) {
- return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
- }
-
- if (is_string($message) || is_numeric($message)) {
- $message = new Text(['content' => $message]);
- }
-
- if (!$this->isMessage($message)) {
- $messageType = gettype($message);
-
- throw new InvalidArgumentException("Invalid Message type .'{$messageType}'");
- }
-
- $response = $this->buildReply($to, $from, $message);
-
- if ($this->isSafeMode()) {
- Log::debug('Message safe mode is enable.');
- $response = $this->encryptor->encryptMsg(
- $response,
- $this->request->get('nonce'),
- $this->request->get('timestamp')
- );
- }
-
- return $response;
- }
-
- /**
- * Whether response is message.
- *
- * @param mixed $message
- *
- * @return bool
- */
- protected function isMessage($message)
- {
- if (is_array($message)) {
- foreach ($message as $element) {
- if (!is_subclass_of($element, AbstractMessage::class)) {
- return false;
- }
- }
-
- return true;
- }
-
- return is_subclass_of($message, AbstractMessage::class);
- }
-
- /**
- * Get request message.
- *
- * @return array
- *
- * @throws BadRequestException
- */
- public function getMessage()
- {
- $message = $this->parseMessageFromRequest($this->request->getContent(false));
-
- if (!is_array($message) || empty($message)) {
- throw new BadRequestException('Invalid request.');
- }
-
- return $message;
- }
-
- /**
- * Handle request.
- *
- * @return array
- *
- * @throws \EasyWeChat\Core\Exceptions\RuntimeException
- * @throws \EasyWeChat\Server\BadRequestException
- */
- protected function handleRequest()
- {
- $message = $this->getMessage();
- $response = $this->handleMessage($message);
-
- $messageType = isset($message['msg_type']) ? $message['msg_type'] : $message['MsgType'];
-
- if ('device_text' === $messageType) {
- $message['FromUserName'] = '';
- $message['ToUserName'] = '';
- }
-
- return [
- 'to' => $message['FromUserName'],
- 'from' => $message['ToUserName'],
- 'response' => $response,
- ];
- }
-
- /**
- * Handle message.
- *
- * @param array $message
- *
- * @return mixed
- */
- protected function handleMessage(array $message)
- {
- $handler = $this->messageHandler;
-
- if (!is_callable($handler)) {
- Log::debug('No handler enabled.');
-
- return null;
- }
-
- Log::debug('Message detail:', $message);
-
- $message = new Collection($message);
-
- $messageType = $message->get('msg_type', $message->get('MsgType'));
-
- $type = $this->messageTypeMapping[$messageType];
-
- $response = null;
-
- if ($this->messageFilter & $type) {
- $response = call_user_func_array($handler, [$message]);
- }
-
- return $response;
- }
-
- /**
- * Build reply XML.
- *
- * @param string $to
- * @param string $from
- * @param AbstractMessage $message
- *
- * @return string
- */
- protected function buildReply($to, $from, $message)
- {
- $base = [
- 'ToUserName' => $to,
- 'FromUserName' => $from,
- 'CreateTime' => time(),
- 'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
- ];
-
- $transformer = new Transformer();
-
- return XML::build(array_merge($base, $transformer->transform($message)));
- }
-
- /**
- * Get signature.
- *
- * @param array $request
- *
- * @return string
- */
- protected function signature($request)
- {
- sort($request, SORT_STRING);
-
- return sha1(implode($request));
- }
-
- /**
- * Parse message array from raw php input.
- *
- * @param string|resource $content
- *
- * @throws \EasyWeChat\Core\Exceptions\RuntimeException
- * @throws \EasyWeChat\Encryption\EncryptionException
- *
- * @return array
- */
- protected function parseMessageFromRequest($content)
- {
- $content = strval($content);
-
- $dataSet = json_decode($content, true);
- if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
- // For mini-program JSON formats.
- // Convert to XML if the given string can be decode into a data array.
- $content = XML::build($dataSet);
- }
-
- if ($this->isSafeMode()) {
- if (!$this->encryptor) {
- throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
- }
-
- $message = $this->encryptor->decryptMsg(
- $this->request->get('msg_signature'),
- $this->request->get('nonce'),
- $this->request->get('timestamp'),
- $content
- );
- } else {
- $message = XML::parse($content);
- }
-
- return $message;
- }
-
- /**
- * Check the request message safe mode.
- *
- * @return bool
- */
- private function isSafeMode()
- {
- return $this->request->get('encrypt_type') && 'aes' === $this->request->get('encrypt_type');
- }
- }
|