Completed
Push — master ( 5ddcc0...9e0ead )
by Carlos
42s
created

Guard::buildResponse()   C

Complexity

Conditions 7
Paths 8

Size

Total Lines 31
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 7.0061

Importance

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