Test Setup Failed
Push — master ( d37a6a...512c0e )
by Carlos
03:19
created

Guard   C

Complexity

Total Complexity 44

Size/Duplication

Total Lines 454
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 16

Test Coverage

Coverage 82.71%

Importance

Changes 0
Metric Value
dl 0
loc 454
rs 6.916
c 0
b 0
f 0
ccs 110
cts 133
cp 0.8271
wmc 44
lcom 1
cbo 16

20 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 2
A debug() 0 6 1
B serve() 0 26 2
A validate() 0 12 3
A setMessageHandler() 0 11 2
A getMessageHandler() 0 4 1
A getRequest() 0 4 1
A setRequest() 0 6 1
A setEncryptor() 0 6 1
A getEncryptor() 0 4 1
C buildResponse() 0 32 8
A isMessage() 0 14 4
A getMessage() 0 10 3
A getCollectedMessage() 0 4 1
A handleRequest() 0 11 1
B handleMessage() 0 24 3
A buildReply() 0 13 2
A signature() 0 6 1
B parseMessageFromRequest() 0 25 4
A isSafeMode() 0 4 2

How to fix   Complexity   

Complex Class

Complex classes like Guard often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Guard, and based on these observations, apply Extract Interface, too.

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