Completed
Push — master ( b7a71d...68e325 )
by Carlos
04:05
created

Guard::getRequest()   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 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
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
     * Request getter.
214
     *
215
     * @return Request
216
     */
217
    public function getRequest()
218
    {
219
        return $this->request;
220
    }
221
222
    /**
223
     * Request setter.
224
     *
225
     * @param Request $request
226
     *
227
     * @return $this
228
     */
229
    public function setRequest(Request $request)
230
    {
231
        $this->request = $request;
232
233
        return $this;
234
    }
235
236
    /**
237
     * Set Encryptor.
238
     *
239
     * @param Encryptor $encryptor
240
     *
241
     * @return Guard
242
     */
243 1
    public function setEncryptor(Encryptor $encryptor)
244
    {
245 1
        $this->encryptor = $encryptor;
246
247 1
        return $this;
248
    }
249
250
    /**
251
     * Return the encryptor instance.
252
     *
253
     * @return Encryptor
254
     */
255
    public function getEncryptor()
256
    {
257
        return $this->encryptor;
258
    }
259
260
    /**
261
     * Build response.
262
     *
263
     * @param $to
264
     * @param $from
265
     * @param mixed $message
266
     *
267
     * @return string
268
     *
269
     * @throws \EasyWeChat\Core\Exceptions\InvalidArgumentException
270
     */
271 6
    protected function buildResponse($to, $from, $message)
272
    {
273 6
        if (empty($message) || $message === self::SUCCESS_EMPTY_RESPONSE) {
274 3
            return self::SUCCESS_EMPTY_RESPONSE;
275
        }
276
277 5
        if ($message instanceof RawMessage) {
278 1
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
279
        }
280
281 4
        if (is_string($message) || is_numeric($message)) {
282 4
            $message = new Text(['content' => $message]);
283 4
        }
284
285 4
        if (!$this->isMessage($message)) {
286
            throw new InvalidArgumentException("Invalid Message type .'{gettype($message)}'");
287
        }
288
289 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...
290
291 4
        if ($this->isSafeMode()) {
292 1
            Log::debug('Message safe mode is enable.');
293 1
            $response = $this->encryptor->encryptMsg(
294 1
                $response,
295 1
                $this->request->get('nonce'),
296 1
                $this->request->get('timestamp')
297 1
            );
298 1
        }
299
300 4
        return $response;
301
    }
302
303
    /**
304
     * Whether response is message.
305
     *
306
     * @param mixed $message
307
     *
308
     * @return bool
309
     */
310 4
    protected function isMessage($message)
311
    {
312 4
        if (is_array($message)) {
313
            foreach ($message as $element) {
314
                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...
315
                    return false;
316
                }
317
            }
318
319
            return true;
320
        }
321
322 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...
323
    }
324
325
    /**
326
     * Get request message.
327
     *
328
     * @return object
329
     */
330 6
    public function getMessage()
331
    {
332 6
        $message = $this->parseMessageFromRequest($this->request->getContent(false));
333
334 6
        if (!is_array($message) || empty($message)) {
335
            throw new BadRequestException('Invalid request.');
336
        }
337
338 6
        return $message;
339
    }
340
341
    /**
342
     * Handle request.
343
     *
344
     * @return array
345
     *
346
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
347
     * @throws \EasyWeChat\Server\BadRequestException
348
     */
349 6
    protected function handleRequest()
350
    {
351 6
        $message = $this->getMessage();
352 6
        $response = $this->handleMessage($message);
353
354
        return [
355 6
            'to' => $message['FromUserName'],
356 6
            'from' => $message['ToUserName'],
357 6
            'response' => $response,
358 6
        ];
359
    }
360
361
    /**
362
     * Handle message.
363
     *
364
     * @param array $message
365
     *
366
     * @return mixed
367
     */
368 6
    protected function handleMessage($message)
369
    {
370 6
        $handler = $this->messageHandler;
371
372 6
        if (!is_callable($handler)) {
373 3
            Log::debug('No handler enabled.');
374
375 3
            return;
376
        }
377
378 5
        Log::debug('Message detail:', $message);
379
380 5
        $message = new Collection($message);
381
382 5
        $type = $this->messageTypeMapping[$message->get('MsgType')];
383
384 5
        $response = null;
385
386 5
        if ($this->messageFilter & $type) {
387 5
            $response = call_user_func_array($handler, [$message]);
388 5
        }
389
390 5
        return $response;
391
    }
392
393
    /**
394
     * Build reply XML.
395
     *
396
     * @param string          $to
397
     * @param string          $from
398
     * @param AbstractMessage $message
399
     *
400
     * @return string
401
     */
402 4
    protected function buildReply($to, $from, $message)
403
    {
404
        $base = [
405 4
            'ToUserName' => $to,
406 4
            'FromUserName' => $from,
407 4
            'CreateTime' => time(),
408 4
            'MsgType' => is_array($message) ? current($message)->getType() : $message->getType(),
409 4
        ];
410
411 4
        $transformer = new Transformer();
412
413 4
        return XML::build(array_merge($base, $transformer->transform($message)));
414
    }
415
416
    /**
417
     * Get signature.
418
     *
419
     * @param array $request
420
     *
421
     * @return string
422
     */
423 6
    protected function signature($request)
424
    {
425 6
        sort($request, SORT_STRING);
426
427 6
        return sha1(implode($request));
428
    }
429
430
    /**
431
     * Parse message array from raw php input.
432
     *
433
     * @param string|resource $content
434
     *
435
     * @throws \EasyWeChat\Core\Exceptions\RuntimeException
436
     * @throws \EasyWeChat\Encryption\EncryptionException
437
     *
438
     * @return array
439
     */
440 6
    protected function parseMessageFromRequest($content)
441
    {
442 6
        $content = strval($content);
443
444 6
        if ($this->isSafeMode()) {
445 1
            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 1
            $message = $this->encryptor->decryptMsg(
450 1
                $this->request->get('msg_signature'),
451 1
                $this->request->get('nonce'),
452 1
                $this->request->get('timestamp'),
453
                $content
454 1
            );
455 1
        } else {
456 5
            $message = XML::parse($content);
457
        }
458
459 6
        return $message;
460
    }
461
462
    /**
463
     * Check the request message safe mode.
464
     *
465
     * @return bool
466
     */
467 6
    private function isSafeMode()
468
    {
469 6
        return $this->request->get('encrypt_type') && $this->request->get('encrypt_type') === 'aes';
470
    }
471
}
472