Completed
Push — master ( 73ed3f...e38e9f )
by Carlos
02:46
created

Guard::getCollectedMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
c 0
b 0
f 0
ccs 0
cts 2
cp 0
crap 2
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
 * @see      https://github.com/overtrue
19
 * @see      http://overtrue.me
20
 */
21
22
namespace EasyWeChat\Server;
23
24
use EasyWeChat\Core\Exceptions\FaultException;
25
use EasyWeChat\Core\Exceptions\InvalidArgumentException;
26
use EasyWeChat\Core\Exceptions\RuntimeException;
27
use EasyWeChat\Encryption\Encryptor;
28
use EasyWeChat\Message\AbstractMessage;
29
use EasyWeChat\Message\Raw as RawMessage;
30
use EasyWeChat\Message\Text;
31
use EasyWeChat\Support\Collection;
32
use EasyWeChat\Support\Log;
33
use EasyWeChat\Support\XML;
34
use Symfony\Component\HttpFoundation\Request;
35
use Symfony\Component\HttpFoundation\Response;
36
37
/**
38
 * Class Guard.
39
 */
40
class Guard
41
{
42
    /**
43
     * Empty string.
44
     */
45
    const SUCCESS_EMPTY_RESPONSE = 'success';
46
47
    const TEXT_MSG = 2;
48
    const IMAGE_MSG = 4;
49
    const VOICE_MSG = 8;
50
    const VIDEO_MSG = 16;
51
    const SHORT_VIDEO_MSG = 32;
52
    const LOCATION_MSG = 64;
53
    const LINK_MSG = 128;
54
    const DEVICE_EVENT_MSG = 256;
55
    const DEVICE_TEXT_MSG = 512;
56
    const EVENT_MSG = 1048576;
57
    const ALL_MSG = 1049598;
58
59
    /**
60
     * @var Request
61
     */
62
    protected $request;
63
64
    /**
65
     * @var string
66
     */
67
    protected $token;
68
69
    /**
70
     * @var Encryptor
71
     */
72
    protected $encryptor;
73
74
    /**
75
     * @var string|callable
76
     */
77
    protected $messageHandler;
78
79
    /**
80
     * @var int
81
     */
82
    protected $messageFilter;
83
84
    /**
85
     * @var array
86
     */
87
    protected $messageTypeMapping = [
88
        'text' => 2,
89
        'image' => 4,
90
        'voice' => 8,
91
        'video' => 16,
92
        'shortvideo' => 32,
93
        'location' => 64,
94
        'link' => 128,
95
        'device_event' => 256,
96
        'device_text' => 512,
97
        'event' => 1048576,
98
    ];
99
100
    /**
101
     * @var bool
102
     */
103
    protected $debug = false;
104
105
    /**
106
     * Constructor.
107
     *
108
     * @param string  $token
109
     * @param Request $request
110
     */
111 7
    public function __construct($token, Request $request = null)
112
    {
113 7
        $this->token = $token;
114 7
        $this->request = $request ?: Request::createFromGlobals();
115 7
    }
116
117
    /**
118
     * Enable/Disable debug mode.
119
     *
120
     * @param bool $debug
121
     *
122
     * @return $this
123
     */
124
    public function debug($debug = true)
125
    {
126
        $this->debug = $debug;
127
128
        return $this;
129
    }
130
131
    /**
132
     * Handle and return response.
133
     *
134
     * @return Response
135
     *
136
     * @throws BadRequestException
137
     */
138 6
    public function serve()
139
    {
140 6
        Log::debug('Request received:', [
141 6
            'Method' => $this->request->getMethod(),
142 6
            'URI' => $this->request->getRequestUri(),
143 6
            'Query' => $this->request->getQueryString(),
144 6
            'Protocal' => $this->request->server->get('SERVER_PROTOCOL'),
145 6
            'Content' => $this->request->getContent(),
146 6
        ]);
147
148 6
        $this->validate($this->token);
149
150 6
        if ($str = $this->request->get('echostr')) {
151 1
            Log::debug("Output 'echostr' is '$str'.");
152
153 1
            return new Response($str);
154
        }
155
156 6
        $result = $this->handleRequest();
157
158 6
        $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
159
160 6
        Log::debug('Server response created:', compact('response'));
161
162 6
        return new Response($response);
163
    }
164
165
    /**
166
     * Validation request params.
167
     *
168
     * @param string $token
169
     *
170
     * @throws FaultException
171
     */
172 6
    public function validate($token)
173
    {
174
        $params = [
175 6
            $token,
176 6
            $this->request->get('timestamp'),
177 6
            $this->request->get('nonce'),
178 6
        ];
179
180 6
        if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
181
            throw new FaultException('Invalid request signature.', 400);
182
        }
183 6
    }
184
185
    /**
186
     * Add a event listener.
187
     *
188
     * @param callable $callback
189
     * @param int      $option
190
     *
191
     * @return Guard
192
     *
193
     * @throws InvalidArgumentException
194
     */
195 6
    public function setMessageHandler($callback = null, $option = self::ALL_MSG)
196
    {
197 6
        if (!is_callable($callback)) {
198 1
            throw new InvalidArgumentException('Argument #2 is not callable.');
199
        }
200
201 6
        $this->messageHandler = $callback;
202 6
        $this->messageFilter = $option;
203
204 6
        return $this;
205
    }
206
207
    /**
208
     * Return the message listener.
209
     *
210
     * @return string
211
     */
212 1
    public function getMessageHandler()
213
    {
214 1
        return $this->messageHandler;
215
    }
216
217
    /**
218
     * Request getter.
219
     *
220
     * @return Request
221
     */
222
    public function getRequest()
223
    {
224
        return $this->request;
225
    }
226
227
    /**
228
     * Request setter.
229
     *
230
     * @param Request $request
231
     *
232
     * @return $this
233
     */
234
    public function setRequest(Request $request)
235
    {
236
        $this->request = $request;
237
238
        return $this;
239
    }
240
241
    /**
242
     * Set Encryptor.
243
     *
244
     * @param Encryptor $encryptor
245
     *
246
     * @return Guard
247
     */
248 1
    public function setEncryptor(Encryptor $encryptor)
249
    {
250 1
        $this->encryptor = $encryptor;
251
252 1
        return $this;
253
    }
254
255
    /**
256
     * Return the encryptor instance.
257
     *
258
     * @return Encryptor
259
     */
260
    public function getEncryptor()
261
    {
262
        return $this->encryptor;
263
    }
264
265
    /**
266
     * Build response.
267
     *
268
     * @param $to
269
     * @param $from
270
     * @param mixed $message
271
     *
272
     * @return string
273
     *
274
     * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
275
     */
276 6
    protected function buildResponse($to, $from, $message)
277
    {
278 6
        if (empty($message) || $message === self::SUCCESS_EMPTY_RESPONSE) {
279 3
            return self::SUCCESS_EMPTY_RESPONSE;
280
        }
281
282 5
        if ($message instanceof RawMessage) {
283 1
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
284
        }
285
286 4
        if (is_string($message) || is_numeric($message)) {
287 4
            $message = new Text(['content' => $message]);
288 4
        }
289
290 4
        if (!$this->isMessage($message)) {
291
            $messageType = gettype($message);
292
            throw new InvalidArgumentException("Invalid Message type .'{$messageType}'");
293
        }
294
295 4
        $response = $this->buildReply($to, $from, $message);
0 ignored issues
show
Documentation introduced by
$message is of type object|null|array|boolean, but the function expects a object<EasyWeChat\Message\AbstractMessage>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
296
297 4
        if ($this->isSafeMode()) {
298 1
            Log::debug('Message safe mode is enable.');
299 1
            $response = $this->encryptor->encryptMsg(
300 1
                $response,
301 1
                $this->request->get('nonce'),
302 1
                $this->request->get('timestamp')
303 1
            );
304 1
        }
305
306 4
        return $response;
307
    }
308
309
    /**
310
     * Whether response is message.
311
     *
312
     * @param mixed $message
313
     *
314
     * @return bool
315
     */
316 4
    protected function isMessage($message)
317
    {
318 4
        if (is_array($message)) {
319
            foreach ($message as $element) {
320
                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...
321
                    return false;
322
                }
323
            }
324
325
            return true;
326
        }
327
328 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...
329
    }
330
331
    /**
332
     * Get request message.
333
     *
334
     * @return array
335
     *
336
     * @throws BadRequestException
337
     */
338 6
    public function getMessage()
339
    {
340 6
        $message = $this->parseMessageFromRequest($this->request->getContent(false));
341
342 6
        if (!is_array($message) || empty($message)) {
343
            throw new BadRequestException('Invalid request.');
344
        }
345
346 6
        return $message;
347
    }
348
349
    /**
350
     * Get the collected request message.
351
     *
352
     * @return Collection
353
     */
354
    public function getCollectedMessage()
355
    {
356
        return new Collection($this->getMessage());
357
    }
358
359
    /**
360
     * Handle request.
361
     *
362
     * @return array
363
     *
364
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
365
     * @throws \EasyWeChat\Server\BadRequestException
366
     */
367 6
    protected function handleRequest()
368
    {
369 6
        $message = $this->getMessage();
370 6
        $response = $this->handleMessage($message);
371
372
        return [
373 6
            'to' => $message['FromUserName'],
374 6
            'from' => $message['ToUserName'],
375 6
            'response' => $response,
376 6
        ];
377
    }
378
379
    /**
380
     * Handle message.
381
     *
382
     * @param array $message
383
     *
384
     * @return mixed
385
     */
386 6
    protected function handleMessage($message)
387
    {
388 6
        $handler = $this->messageHandler;
389
390 6
        if (!is_callable($handler)) {
391 3
            Log::debug('No handler enabled.');
392
393 3
            return;
394
        }
395
396 5
        Log::debug('Message detail:', $message);
397
398 5
        $message = new Collection($message);
399
400 5
        $type = $this->messageTypeMapping[$message->get('MsgType')];
401
402 5
        $response = null;
403
404 5
        if ($this->messageFilter & $type) {
405 5
            $response = call_user_func_array($handler, [$message]);
406 5
        }
407
408 5
        return $response;
409
    }
410
411
    /**
412
     * Build reply XML.
413
     *
414
     * @param string          $to
415
     * @param string          $from
416
     * @param AbstractMessage $message
417
     *
418
     * @return string
419
     */
420 4
    protected function buildReply($to, $from, $message)
421
    {
422
        $base = [
423 4
            'ToUserName' => $to,
424 4
            'FromUserName' => $from,
425 4
            'CreateTime' => time(),
426 4
            'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
427 4
        ];
428
429 4
        $transformer = new Transformer();
430
431 4
        return XML::build(array_merge($base, $transformer->transform($message)));
432
    }
433
434
    /**
435
     * Get signature.
436
     *
437
     * @param array $request
438
     *
439
     * @return string
440
     */
441 6
    protected function signature($request)
442
    {
443 6
        sort($request, SORT_STRING);
444
445 6
        return sha1(implode($request));
446
    }
447
448
    /**
449
     * Parse message array from raw php input.
450
     *
451
     * @param string|resource $content
452
     *
453
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
454
     * @throws \EasyWeChat\Encryption\EncryptionException
455
     *
456
     * @return array
457
     */
458 6
    protected function parseMessageFromRequest($content)
459
    {
460 6
        $content = strval($content);
461
462 6
        if ($this->isSafeMode()) {
463 1
            if (!$this->encryptor) {
464
                throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
465
            }
466
467 1
            $message = $this->encryptor->decryptMsg(
468 1
                $this->request->get('msg_signature'),
469 1
                $this->request->get('nonce'),
470 1
                $this->request->get('timestamp'),
471
                $content
472 1
            );
473 1
        } else {
474 5
            $message = XML::parse($content);
475
        }
476
477 6
        return $message;
478
    }
479
480
    /**
481
     * Check the request message safe mode.
482
     *
483
     * @return bool
484
     */
485 6
    private function isSafeMode()
486
    {
487 6
        return $this->request->get('encrypt_type') && $this->request->get('encrypt_type') === 'aes';
488
    }
489
}
490