Completed
Push — master ( 768d7a...23eb1f )
by frey
03:45
created

Guard::parseMessageFromRequest()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

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