<?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.
*/
namespace EasyWeChat\Kernel;
use EasyWeChat\Kernel\Contracts\MessageInterface;
use EasyWeChat\Kernel\Exceptions\BadRequestException;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Kernel\Messages\Message;
use EasyWeChat\Kernel\Messages\News;
use EasyWeChat\Kernel\Messages\NewsItem;
use EasyWeChat\Kernel\Messages\Raw as RawMessage;
use EasyWeChat\Kernel\Messages\Text;
use EasyWeChat\Kernel\Support\XML;
use EasyWeChat\Kernel\Traits\Observable;
use EasyWeChat\Kernel\Traits\ResponseCastable;
use Symfony\Component\HttpFoundation\Response;
* Class ServerGuard.
*
* 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有
* 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的
*
* @author overtrue <i@overtrue.me>
*/
class ServerGuard
{
use Observable;
use ResponseCastable;
* @var bool
*/
protected $alwaysValidate = false;
* Empty string.
*/
const SUCCESS_EMPTY_RESPONSE = 'success';
* @var array
*/
const MESSAGE_TYPE_MAPPING = ['text' => Message::TEXT, 'image' => Message::IMAGE, 'voice' => Message::VOICE, 'video' => Message::VIDEO, 'shortvideo' => Message::SHORT_VIDEO, 'location' => Message::LOCATION, 'link' => Message::LINK, 'device_event' => Message::DEVICE_EVENT, 'device_text' => Message::DEVICE_TEXT, 'event' => Message::EVENT, 'file' => Message::FILE, 'miniprogrampage' => Message::MINIPROGRAM_PAGE];
* @var \EasyWeChat\Kernel\ServiceContainer
*/
protected $app;
* Constructor.
*
* @codeCoverageIgnore
*
* @param \EasyWeChat\Kernel\ServiceContainer $app
*/
public function __construct(ServiceContainer $app)
{
$this->app = $app;
foreach ($this->app->extension->observers() as $observer) {
call_user_func_array([$this, 'push'], $observer);
}
}
* Handle and return response.
*
* @return Response
*
* @throws BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
public function serve()
{
$this->app['logger']->debug('Request received:', ['method' => $this->app['request']->getMethod(), 'uri' => $this->app['request']->getUri(), 'content-type' => $this->app['request']->getContentType(), 'content' => $this->app['request']->getContent()]);
$response = $this->validate()->resolve();
$this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
return $response;
}
* @return $this
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
*/
public function validate()
{
if (!$this->alwaysValidate && !$this->isSafeMode()) {
return $this;
}
if ($this->app['request']->get('signature') !== $this->signature([$this->getToken(), $this->app['request']->get('timestamp'), $this->app['request']->get('nonce')])) {
throw new BadRequestException('Invalid request signature.', 400);
}
return $this;
}
* Force validate request.
*
* @return $this
*/
public function forceValidate()
{
$this->alwaysValidate = true;
return $this;
}
* Get request message.
*
* @return array|\EasyWeChat\Kernel\Support\Collection|object|string
*
* @throws BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
public function getMessage()
{
$message = $this->parseMessage($this->app['request']->getContent(false));
if (!is_array($message) || empty($message)) {
throw new BadRequestException('No message received.');
}
if ($this->isSafeMode() && !empty($message['Encrypt'])) {
$message = $this->decryptMessage($message);
$dataSet = json_decode($message, true);
if ($dataSet && JSON_ERROR_NONE === json_last_error()) {
return $dataSet;
}
$message = XML::parse($message);
}
return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
}
* Resolve server request and return the response.
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function resolve()
{
$result = $this->handleRequest();
if ($this->shouldReturnRawResponse()) {
$response = new Response($result['response']);
} else {
$response = new Response($this->buildResponse($result['to'], $result['from'], $result['response']), 200, ['Content-Type' => 'application/xml']);
}
return $response;
}
* @return string|null
*/
protected function getToken()
{
return $this->app['config']['token'];
}
* @param $to
* @param $from
* @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
*
* @return string
*
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
*/
public 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((string) $message);
}
if (is_array($message) && reset($message) instanceof NewsItem) {
$message = new News($message);
}
if (!$message instanceof Message) {
throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
}
return $this->buildReply($to, $from, $message);
}
* Handle request.
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
* @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
*/
protected function handleRequest()
{
$castedMessage = $this->getMessage();
$messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
$response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[!empty($messageArray['MsgType']) ? $messageArray['MsgType'] : (isset($messageArray['msg_type']) ? $messageArray['msg_type'] : 'text')], $castedMessage);
return ['to' => !empty($messageArray['FromUserName']) ? $messageArray['FromUserName'] : '', 'from' => !empty($messageArray['ToUserName']) ? $messageArray['ToUserName'] : '', 'response' => $response];
}
* Build reply XML.
*
* @param $to
* @param $from
* @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
*
* @return string
*/
protected function buildReply($to, $from, MessageInterface $message)
{
$prepends = ['ToUserName' => $to, 'FromUserName' => $from, 'CreateTime' => time(), 'MsgType' => $message->getType()];
$response = $message->transformToXml($prepends);
if ($this->isSafeMode()) {
$this->app['logger']->debug('Messages safe mode is enabled.');
$response = $this->app['encryptor']->encrypt($response);
}
return $response;
}
* @param array $params
*
* @return string
*/
protected function signature(array $params)
{
sort($params, SORT_STRING);
return sha1(implode($params));
}
* Parse message array from raw php input.
*
* @param $content
*
* @return array
*
* @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
*/
protected function parseMessage($content)
{
try {
if (0 === stripos($content, '<')) {
$content = XML::parse($content);
} else {
$dataSet = json_decode($content, true);
if ($dataSet && JSON_ERROR_NONE === json_last_error()) {
$content = $dataSet;
}
}
return (array) $content;
} catch (\Exception $e) {
throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
}
}
* Check the request message safe mode.
*
* @return bool
*/
protected function isSafeMode()
{
return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
}
* @return bool
*/
protected function shouldReturnRawResponse()
{
return false;
}
* @param array $message
*
* @return mixed
*/
protected function decryptMessage(array $message)
{
return $message = $this->app['encryptor']->decrypt($message['Encrypt'], $this->app['request']->get('msg_signature'), $this->app['request']->get('nonce'), $this->app['request']->get('timestamp'));
}
}