Completed
Pull Request — master (#377)
by Carlos
09:03 queued 05:52
created

Guard::debug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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