Guard::setMessageHandler()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 2
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace YEntWeChat\Server;
4
5
use YEntWeChat\Core\Exceptions\FaultException;
6
use YEntWeChat\Core\Exceptions\InvalidArgumentException;
7
use YEntWeChat\Core\Exceptions\RuntimeException;
8
use YEntWeChat\Encryption\Encryptor;
9
use YEntWeChat\Message\AbstractMessage;
10
use YEntWeChat\Message\Raw as RawMessage;
11
use YEntWeChat\Message\Text;
12
use YEntWeChat\Support\Collection;
13
use YEntWeChat\Support\Log;
14
use YEntWeChat\Support\XML;
15
use Symfony\Component\HttpFoundation\Request;
16
use Symfony\Component\HttpFoundation\Response;
17
18
/**
19
 * Class Guard.
20
 */
21
class Guard
22
{
23
    /**
24
     * Empty string.
25
     */
26
    const SUCCESS_EMPTY_RESPONSE = 'success';
27
28
    const TEXT_MSG = 2;
29
    const IMAGE_MSG = 4;
30
    const VOICE_MSG = 8;
31
    const VIDEO_MSG = 16;
32
    const SHORT_VIDEO_MSG = 32;
33
    const LOCATION_MSG = 64;
34
    const LINK_MSG = 128;
35
    const DEVICE_EVENT_MSG = 256;
36
    const DEVICE_TEXT_MSG = 512;
37
    const EVENT_MSG = 1048576;
38
    const ALL_MSG = 1049598;
39
40
    /**
41
     * @var Request
42
     */
43
    protected $request;
44
45
    /**
46
     * @var string
47
     */
48
    protected $token;
49
50
    /**
51
     * @var Encryptor
52
     */
53
    protected $encryptor;
54
55
    /**
56
     * @var string|callable
57
     */
58
    protected $messageHandler;
59
60
    /**
61
     * @var int
62
     */
63
    protected $messageFilter;
64
65
    /**
66
     * @var array
67
     */
68
    protected $messageTypeMapping = [
69
        'text'         => 2,
70
        'image'        => 4,
71
        'voice'        => 8,
72
        'video'        => 16,
73
        'shortvideo'   => 32,
74
        'location'     => 64,
75
        'link'         => 128,
76
        'device_event' => 256,
77
        'device_text'  => 512,
78
        'event'        => 1048576,
79
    ];
80
81
    /**
82
     * @var bool
83
     */
84
    protected $debug = false;
85
86
    /**
87
     * Constructor.
88
     *
89
     * @param string  $token
90
     * @param Request $request
91
     */
92
    public function __construct($token, Request $request = null)
93
    {
94
        $this->token = $token;
95
        $this->request = $request ?: Request::createFromGlobals();
96
    }
97
98
    /**
99
     * Enable/Disable debug mode.
100
     *
101
     * @param bool $debug
102
     *
103
     * @return $this
104
     */
105
    public function debug($debug = true)
106
    {
107
        $this->debug = $debug;
108
109
        return $this;
110
    }
111
112
    /**
113
     * Handle and return response.
114
     *
115
     * @throws BadRequestException
116
     *
117
     * @return Response
118
     */
119
    public function serve()
120
    {
121
        Log::debug('Request received:', [
122
            'Method'   => $this->request->getMethod(),
123
            'URI'      => $this->request->getRequestUri(),
124
            'Query'    => $this->request->getQueryString(),
125
            'Protocol' => $this->request->server->get('SERVER_PROTOCOL'),
126
            'Content'  => $this->request->getContent(),
127
        ]);
128
129
        $this->validate($this->token);
130
131
        if ($str = $this->request->get('echostr')) {
132
            $str = $this->encryptor->decryptEcho(
133
                $this->request->get('msg_signature'),
134
                $this->request->get('nonce'),
135
                $this->request->get('timestamp'),
136
                $str
137
            );
138
            Log::debug("Output 'echostr' is '$str'.");
139
140
            return new Response($str);
141
        }
142
143
        $result = $this->handleRequest();
144
145
        $response = $this->buildResponse($result['to'], $result['from'], $result['response']);
146
147
        Log::debug('Server response created:', compact('response'));
148
149
        return new Response($response);
150
    }
151
152
    /**
153
     * Validation request params.
154
     *
155
     * @param string $token
156
     *
157
     * @throws FaultException
158
     */
159
    public function validate($token)
160
    {
161
        $params = [
162
            $token,
163
            $this->request->get('timestamp'),
164
            $this->request->get('nonce'),
165
        ];
166
167
        if (!$this->debug && $this->request->get('signature') !== $this->signature($params)) {
168
            throw new FaultException('Invalid request signature.', 400);
169
        }
170
    }
171
172
    /**
173
     * Add a event listener.
174
     *
175
     * @param callable $callback
176
     * @param int      $option
177
     *
178
     * @throws InvalidArgumentException
179
     *
180
     * @return Guard
181
     */
182
    public function setMessageHandler($callback = null, $option = self::ALL_MSG)
183
    {
184
        if (!is_callable($callback)) {
185
            throw new InvalidArgumentException('Argument #2 is not callable.');
186
        }
187
188
        $this->messageHandler = $callback;
189
        $this->messageFilter = $option;
190
191
        return $this;
192
    }
193
194
    /**
195
     * Return the message listener.
196
     *
197
     * @return string
198
     */
199
    public function getMessageHandler()
200
    {
201
        return $this->messageHandler;
202
    }
203
204
    /**
205
     * Request getter.
206
     *
207
     * @return Request
208
     */
209
    public function getRequest()
210
    {
211
        return $this->request;
212
    }
213
214
    /**
215
     * Request setter.
216
     *
217
     * @param Request $request
218
     *
219
     * @return $this
220
     */
221
    public function setRequest(Request $request)
0 ignored issues
show
Bug introduced by
You have injected the Request via parameter $request. This is generally not recommended as there might be multiple instances during a request cycle (f.e. when using sub-requests). Instead, it is recommended to inject the RequestStack and retrieve the current request each time you need it via getCurrentRequest().
Loading history...
222
    {
223
        $this->request = $request;
224
225
        return $this;
226
    }
227
228
    /**
229
     * Set Encryptor.
230
     *
231
     * @param Encryptor $encryptor
232
     *
233
     * @return Guard
234
     */
235
    public function setEncryptor(Encryptor $encryptor)
236
    {
237
        $this->encryptor = $encryptor;
238
239
        return $this;
240
    }
241
242
    /**
243
     * Return the encryptor instance.
244
     *
245
     * @return Encryptor
246
     */
247
    public function getEncryptor()
248
    {
249
        return $this->encryptor;
250
    }
251
252
    /**
253
     * Build response.
254
     *
255
     * @param       $to
256
     * @param       $from
257
     * @param mixed $message
258
     *
259
     * @throws \EntWeChat\Core\Exceptions\InvalidArgumentException
260
     *
261
     * @return string
262
     */
263
    protected function buildResponse($to, $from, $message)
264
    {
265
        if (empty($message) || $message === self::SUCCESS_EMPTY_RESPONSE) {
266
            return self::SUCCESS_EMPTY_RESPONSE;
267
        }
268
269
        if ($message instanceof RawMessage) {
270
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
271
        }
272
273
        if (is_string($message) || is_numeric($message)) {
274
            $message = new Text(['content' => $message]);
275
        }
276
277
        if (!$this->isMessage($message)) {
278
            $messageType = gettype($message);
279
            throw new InvalidArgumentException("Invalid Message type .'{$messageType}'");
280
        }
281
282
        $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<YEntWeChat\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...
283
284
        if ($this->isSafeMode()) {
285
            Log::debug('Message safe mode is enable.');
286
            $response = $this->encryptor->encryptMsg(
287
                $response,
288
                $this->request->get('nonce'),
289
                $this->request->get('timestamp')
290
            );
291
        }
292
293
        return $response;
294
    }
295
296
    /**
297
     * Whether response is message.
298
     *
299
     * @param mixed $message
300
     *
301
     * @return bool
302
     */
303
    protected function isMessage($message)
304
    {
305
        if (is_array($message)) {
306
            foreach ($message as $element) {
307
                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 \YEntWeChat\Message\AbstractMessage::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
308
                    return false;
309
                }
310
            }
311
312
            return true;
313
        }
314
315
        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 \YEntWeChat\Message\AbstractMessage::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
316
    }
317
318
    /**
319
     * Get request message.
320
     *
321
     * @throws BadRequestException
322
     *
323
     * @return array
324
     */
325
    public function getMessage()
326
    {
327
        $message = $this->parseMessageFromRequest($this->request->getContent(false));
328
329
        if (!is_array($message) || empty($message)) {
330
            throw new BadRequestException('Invalid request.');
331
        }
332
333
        return $message;
334
    }
335
336
    /**
337
     * Handle request.
338
     *
339
     * @throws \EntWeChat\Core\Exceptions\RuntimeException
340
     * @throws \EntWeChat\Server\BadRequestException
341
     *
342
     * @return array
343
     */
344
    protected function handleRequest()
345
    {
346
        $message = $this->getMessage();
347
        $response = $this->handleMessage($message);
348
349
        return [
350
            'to'       => $message['FromUserName'],
351
            'from'     => $message['ToUserName'],
352
            'response' => $response,
353
        ];
354
    }
355
356
    /**
357
     * Handle message.
358
     *
359
     * @param array $message
360
     *
361
     * @return mixed
362
     */
363
    protected function handleMessage(array $message)
364
    {
365
        $handler = $this->messageHandler;
366
367
        if (!is_callable($handler)) {
368
            Log::debug('No handler enabled.');
369
370
            return;
371
        }
372
373
        Log::debug('Message detail:', $message);
374
375
        $message = new Collection($message);
376
377
        $type = $this->messageTypeMapping[$message->get('MsgType')];
378
379
        $response = null;
380
381
        if ($this->messageFilter & $type) {
382
            $response = call_user_func_array($handler, [$message]);
383
        }
384
385
        return $response;
386
    }
387
388
    /**
389
     * Build reply XML.
390
     *
391
     * @param string          $to
392
     * @param string          $from
393
     * @param AbstractMessage $message
394
     *
395
     * @return string
396
     */
397
    protected function buildReply($to, $from, $message)
398
    {
399
        $base = [
400
            'ToUserName'   => $to,
401
            'FromUserName' => $from,
402
            'CreateTime'   => time(),
403
            'MsgType'      => is_array($message) ? current($message)->getType() : $message->getType(),
404
        ];
405
406
        $transformer = new Transformer();
407
408
        return XML::build(array_merge($base, $transformer->transform($message)));
409
    }
410
411
    /**
412
     * Get signature.
413
     *
414
     * @param array $request
415
     *
416
     * @return string
417
     */
418
    protected function signature($request)
419
    {
420
        sort($request, SORT_STRING);
421
422
        return sha1(implode($request));
423
    }
424
425
    /**
426
     * Parse message array from raw php input.
427
     *
428
     * @param string|resource $content
429
     *
430
     * @throws \EntWeChat\Core\Exceptions\RuntimeException
431
     * @throws \EntWeChat\Encryption\EncryptionException
432
     *
433
     * @return array
434
     */
435
    protected function parseMessageFromRequest($content)
436
    {
437
        $content = strval($content);
438
439
        $arrayable = json_decode($content, true);
440
        if (json_last_error() === JSON_ERROR_NONE) {
441
            return $arrayable;
442
        }
443
444
        if ($this->isSafeMode()) {
445
            if (!$this->encryptor) {
446
                throw new RuntimeException('Safe mode Encryptor is necessary, please use Guard::setEncryptor(Encryptor $encryptor) set the encryptor instance.');
447
            }
448
449
            $message = $this->encryptor->decryptMsg(
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->encryptor->decryp...timestamp'), $content); of type array|SimpleXMLElement adds the type SimpleXMLElement to the return on line 459 which is incompatible with the return type documented by YEntWeChat\Server\Guard::parseMessageFromRequest of type array.
Loading history...
450
                $this->request->get('msg_signature'),
451
                $this->request->get('nonce'),
452
                $this->request->get('timestamp'),
453
                $content
454
            );
455
        } else {
456
            $message = XML::parse($content);
0 ignored issues
show
Bug Compatibility introduced by
The expression \YEntWeChat\Support\XML::parse($content); of type array|SimpleXMLElement adds the type SimpleXMLElement to the return on line 459 which is incompatible with the return type documented by YEntWeChat\Server\Guard::parseMessageFromRequest of type array.
Loading history...
457
        }
458
459
        return $message;
460
    }
461
462
    /**
463
     * Check the request message safe mode.
464
     *
465
     * @return bool
466
     */
467
    private function isSafeMode()
468
    {
469
        return true;
470
    }
471
}
472