<?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
{
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 = 1049598;
protected $request;
protected $token;
protected $encryptor;
protected $messageHandler;
protected $messageFilter;
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,
];
protected $debug = false;
public function __construct($token, Request $request = null)
{
$this->token = $token;
$this->request = $request ?: Request::createFromGlobals();
}
public function debug($debug = true)
{
$this->debug = $debug;
return $this;
}
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);
}
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);
}
}
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;
}
public function getMessageHandler()
{
return $this->messageHandler;
}
public function getRequest()
{
return $this->request;
}
public function setRequest(Request $request)
{
$this->request = $request;
return $this;
}
public function setEncryptor(Encryptor $encryptor)
{
$this->encryptor = $encryptor;
return $this;
}
public function getEncryptor()
{
return $this->encryptor;
}
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;
}
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);
}
public function getMessage()
{
$message = $this->parseMessageFromRequest($this->request->getContent(false));
if (!is_array($message) || empty($message)) {
throw new BadRequestException('Invalid request.');
}
return $message;
}
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,
];
}
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;
}
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)));
}
protected function signature($request)
{
sort($request, SORT_STRING);
return sha1(implode($request));
}
protected function parseMessageFromRequest($content)
{
$content = strval($content);
$dataSet = json_decode($content, true);
if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
$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;
}
private function isSafeMode()
{
return $this->request->get('encrypt_type') && 'aes' === $this->request->get('encrypt_type');
}
}