Completed
Pull Request — master (#437)
by Carlos
06:53 queued 03:38
created

Guard::getMessage()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 10
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 3
eloc 5
c 1
b 0
f 1
nc 2
nop 0
dl 0
loc 10
ccs 4
cts 5
cp 0.8
crap 3.072
rs 9.4285
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
     * @var Request
58
     */
59
    protected $request;
60
61
    /**
62
     * @var string
63
     */
64
    protected $token;
65
66
    /**
67
     * @var Encryptor
68
     */
69
    protected $encryptor;
70
71
    /**
72
     * @var string|callable
73
     */
74
    protected $messageHandler;
75
76
    /**
77
     * @var int
78
     */
79
    protected $messageFilter;
80
81
    /**
82
     * @var array
83
     */
84
    protected $messageTypeMapping = [
85
        'text' => 2,
86
        'image' => 4,
87
        'voice' => 8,
88
        'video' => 16,
89
        'shortvideo' => 32,
90
        'location' => 64,
91
        'link' => 128,
92
        'event' => 1048576,
93
    ];
94
95
    /**
96
     * @var bool
97
     */
98
    protected $debug = false;
99
100
    /**
101
     * Constructor.
102
     *
103
     * @param string  $token
104
     * @param Request $request
105
     */
106 7
    public function __construct($token, Request $request = null)
107
    {
108 7
        $this->token = $token;
109 7
        $this->request = $request ?: Request::createFromGlobals();
110 7
    }
111
112
    /**
113
     * Enable/Disable debug mode.
114
     *
115
     * @param bool $debug
116
     *
117
     * @return $this
118
     */
119
    public function debug($debug = true)
120
    {
121
        $this->debug = $debug;
122
123
        return $this;
124
    }
125
126
    /**
127
     * Handle and return response.
128
     *
129
     * @return Response
130
     *
131
     * @throws BadRequestException
132
     */
133 6
    public function serve()
134
    {
135 6
        Log::debug('Request received:', [
136 6
            'Method' => $this->request->getMethod(),
137 6
            'URI' => $this->request->getRequestUri(),
138 6
            'Query' => $this->request->getQueryString(),
139 6
            'Protocal' => $this->request->server->get('SERVER_PROTOCOL'),
140 6
            'Content' => $this->request->getContent(),
141 6
        ]);
142
143 6
        $this->validate($this->token);
144
145 6
        if ($str = $this->request->get('echostr')) {
146 1
            Log::debug("Output 'echostr' is '$str'.");
147
148 1
            return new Response($str);
149
        }
150
151 6
        $result = $this->handleRequest();
152
153 6
        $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
154
155 6
        Log::debug('Server response created:', compact('response'));
156
157 6
        return new Response($response);
158
    }
159
160
    /**
161
     * Validation request params.
162
     *
163
     * @param string $token
164
     *
165
     * @throws FaultException
166
     */
167 6
    public function validate($token)
168
    {
169
        $params = [
170 6
            $token,
171 6
            $this->request->get('timestamp'),
172 6
            $this->request->get('nonce'),
173 6
        ];
174
175 6
        if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
176
            throw new FaultException('Invalid request signature.', 400);
177
        }
178 6
    }
179
180
    /**
181
     * Add a event listener.
182
     *
183
     * @param callable $callback
184
     * @param int      $option
185
     *
186
     * @return Guard
187
     *
188
     * @throws InvalidArgumentException
189
     */
190 6
    public function setMessageHandler($callback = null, $option = self::ALL_MSG)
191
    {
192 6
        if (!is_callable($callback)) {
193 1
            throw new InvalidArgumentException('Argument #2 is not callable.');
194
        }
195
196 6
        $this->messageHandler = $callback;
197 6
        $this->messageFilter = $option;
198
199 6
        return $this;
200
    }
201
202
    /**
203
     * Return the message listener.
204
     *
205
     * @return string
206
     */
207 1
    public function getMessageHandler()
208
    {
209 1
        return $this->messageHandler;
210
    }
211
212
    /**
213
     * Set Encryptor.
214
     *
215
     * @param Encryptor $encryptor
216
     *
217
     * @return Guard
218
     */
219 1
    public function setEncryptor(Encryptor $encryptor)
220
    {
221 1
        $this->encryptor = $encryptor;
222
223 1
        return $this;
224
    }
225
226
    /**
227
     * Return the encryptor instance.
228
     *
229
     * @return Encryptor
230
     */
231
    public function getEncryptor()
232
    {
233
        return $this->encryptor;
234
    }
235
236
    /**
237
     * Build response.
238
     *
239
     * @param $to
240
     * @param $from
241
     * @param mixed $message
242
     *
243
     * @return string
244
     *
245
     * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
246
     */
247 6
    protected function buildResponse($to, $from, $message)
248
    {
249 6
        if (empty($message) || $message === self::SUCCESS_EMPTY_RESPONSE) {
250 3
            return self::SUCCESS_EMPTY_RESPONSE;
251
        }
252
253 5
        if ($message instanceof RawMessage) {
254 1
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
255
        }
256
257 4
        if (is_string($message)) {
258 4
            $message = new Text(['content' => $message]);
259 4
        }
260
261 4
        if (!$this->isMessage($message)) {
262
            throw new InvalidArgumentException("Invalid Message type .'{gettype($message)}'");
263
        }
264
265 4
        $response = $this->buildReply($to, $from, $message);
266
267 4
        if ($this->isSafeMode()) {
268 1
            Log::debug('Message safe mode is enable.');
269 1
            $response = $this->encryptor->encryptMsg(
270 1
                $response,
271 1
                $this->request->get('nonce'),
272 1
                $this->request->get('timestamp')
273 1
            );
274 1
        }
275
276 4
        return $response;
277
    }
278
279
    /**
280
     * Whether response is message.
281
     *
282
     * @param mixed $message
283
     *
284
     * @return bool
285
     */
286 4
    protected function isMessage($message)
287
    {
288 4
        if (is_array($message)) {
289
            foreach ($message as $element) {
290
                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...
291
                    return false;
292
                }
293
            }
294
295
            return true;
296
        }
297
298 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...
299
    }
300
301
    /**
302
     * Get request message.
303
     *
304
     * @return object
305
     */
306 6
    public function getMessage()
307
    {
308 6
        $message = $this->parseMessageFromRequest($this->request->getContent(false));
309
310 6
        if (!is_array($message) || empty($message)) {
311
            throw new BadRequestException('Invalid request.');
312
        }
313
314 6
        return $message;
315
    }
316
317
    /**
318
     * Handle request.
319
     *
320
     * @return array
321
     *
322
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
323
     * @throws \EasyWeChat\Server\BadRequestException
324
     */
325 6
    protected function handleRequest()
326
    {
327 6
        $message = $this->getMessage();
328 6
        $response = $this->handleMessage($message);
329
330
        return [
331 6
            'to' => $message['FromUserName'],
332 6
            'from' => $message['ToUserName'],
333 6
            'response' => $response,
334 6
        ];
335
    }
336
337
    /**
338
     * Handle message.
339
     *
340
     * @param array $message
341
     *
342
     * @return mixed
343
     */
344 6
    protected function handleMessage($message)
345
    {
346 6
        $handler = $this->messageHandler;
347
348 6
        if (!is_callable($handler)) {
349 3
            Log::debug('No handler enabled.');
350
351 3
            return;
352
        }
353
354 5
        Log::debug('Message detail:', $message);
355
356 5
        $message = new Collection($message);
357
358 5
        $type = $this->messageTypeMapping[$message->get('MsgType')];
359
360 5
        $response = null;
361
362 5
        if ($this->messageFilter & $type) {
363 5
            $response = call_user_func_array($handler, [$message]);
364 5
        }
365
366 5
        return $response;
367
    }
368
369
    /**
370
     * Build reply XML.
371
     *
372
     * @param string          $to
373
     * @param string          $from
374
     * @param AbstractMessage $message
375
     *
376
     * @return string
377
     */
378 4
    protected function buildReply($to, $from, $message)
379
    {
380
        $base = [
381 4
            'ToUserName' => $to,
382 4
            'FromUserName' => $from,
383 4
            'CreateTime' => time(),
384 4
            'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
385 4
        ];
386
387 4
        $transformer = new Transformer();
388
389 4
        return XML::build(array_merge($base, $transformer->transform($message)));
390
    }
391
392
    /**
393
     * Get signature.
394
     *
395
     * @param array $request
396
     *
397
     * @return string
398
     */
399 6
    protected function signature($request)
400
    {
401 6
        sort($request, SORT_STRING);
402
403 6
        return sha1(implode($request));
404
    }
405
406
    /**
407
     * Parse message array from raw php input.
408
     *
409
     * @param string|resource $content
410
     *
411
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
412
     * @throws \EasyWeChat\Encryption\EncryptionException
413
     *
414
     * @return array
415
     */
416 6
    protected function parseMessageFromRequest($content)
417
    {
418 6
        $content = strval($content);
419
420 6
        if ($this->isSafeMode()) {
421 1
            if (!$this->encryptor) {
422
                throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
423
            }
424
425 1
            $message = $this->encryptor->decryptMsg(
426 1
                $this->request->get('msg_signature'),
427 1
                $this->request->get('nonce'),
428 1
                $this->request->get('timestamp'),
429
                $content
430 1
            );
431 1
        } else {
432 5
            $message = XML::parse($content);
433
        }
434
435 6
        return $message;
436
    }
437
438
    /**
439
     * Check the request message safe mode.
440
     *
441
     * @return bool
442
     */
443 6
    private function isSafeMode()
444
    {
445 6
        return $this->request->get('encrypt_type') && $this->request->get('encrypt_type') === 'aes';
446
    }
447
}
448