Passed
Push — master ( e1047d...cb7bda )
by Carlos
02:50
created

ServerGuard::decryptMessage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
ccs 0
cts 0
cp 0
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
namespace EasyWeChat\Kernel;
13
14
use EasyWeChat\Kernel\Contracts\MessageInterface;
15
use EasyWeChat\Kernel\Exceptions\BadRequestException;
16
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
17
use EasyWeChat\Kernel\Messages\Message;
18
use EasyWeChat\Kernel\Messages\News;
19
use EasyWeChat\Kernel\Messages\NewsItem;
20
use EasyWeChat\Kernel\Messages\Raw as RawMessage;
21
use EasyWeChat\Kernel\Messages\Text;
22
use EasyWeChat\Kernel\Support\XML;
23
use EasyWeChat\Kernel\Traits\Observable;
24
use EasyWeChat\Kernel\Traits\ResponseCastable;
25
use Symfony\Component\HttpFoundation\Response;
26
27
/**
28
 * Class ServerGuard.
29
 *
30
 * 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有
31
 * 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的
32
 *
33
 * @author overtrue <[email protected]>
34
 */
35
class ServerGuard
36
{
37
    use Observable, ResponseCastable;
0 ignored issues
show
Bug introduced by
The trait EasyWeChat\Kernel\Traits\Observable requires the property $content which is not provided by EasyWeChat\Kernel\ServerGuard.
Loading history...
38
39
    /**
40
     * @var bool
41
     */
42
    protected $alwaysValidate = false;
43
44
    /**
45
     * Empty string.
46
     */
47
    const SUCCESS_EMPTY_RESPONSE = 'success';
48
49
    /**
50
     * @var array
51
     */
52
    const MESSAGE_TYPE_MAPPING = [
53
        'text' => Message::TEXT,
54
        'image' => Message::IMAGE,
55
        'voice' => Message::VOICE,
56
        'video' => Message::VIDEO,
57
        'shortvideo' => Message::SHORT_VIDEO,
58
        'location' => Message::LOCATION,
59
        'link' => Message::LINK,
60
        'device_event' => Message::DEVICE_EVENT,
61
        'device_text' => Message::DEVICE_TEXT,
62
        'event' => Message::EVENT,
63
        'file' => Message::FILE,
64
        'miniprogrampage' => Message::MINIPROGRAM_PAGE,
65
    ];
66
67
    /**
68
     * @var \EasyWeChat\Kernel\ServiceContainer
69
     */
70
    protected $app;
71
72
    /**
73
     * Constructor.
74
     *
75
     * @codeCoverageIgnore
76
     *
77
     * @param \EasyWeChat\Kernel\ServiceContainer $app
78
     */
79
    public function __construct(ServiceContainer $app)
80
    {
81
        $this->app = $app;
82
83
        foreach ($this->app->extension->observers() as $observer) {
0 ignored issues
show
Bug Best Practice introduced by
The property extension does not exist on EasyWeChat\Kernel\ServiceContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
84
            call_user_func_array([$this, 'push'], $observer);
85
        }
86
    }
87
88
    /**
89
     * Handle and return response.
90
     *
91
     * @return Response
92
     *
93
     * @throws BadRequestException
94
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
95
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
96
     */
97
    public function serve(): Response
98
    {
99
        $this->app['logger']->debug('Request received:', [
100
            'method' => $this->app['request']->getMethod(),
101
            'uri' => $this->app['request']->getUri(),
102
            'content-type' => $this->app['request']->getContentType(),
103
            'content' => $this->app['request']->getContent(),
104
        ]);
105
106
        $response = $this->validate()->resolve();
107
108
        $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
109
110
        return $response;
111
    }
112
113
    /**
114
     * @return $this
115
     *
116
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
117
     */
118
    public function validate()
119
    {
120
        if (!$this->alwaysValidate && !$this->isSafeMode()) {
121
            return $this;
122
        }
123
124
        if ($this->app['request']->get('signature') !== $this->signature([
125
                $this->getToken(),
126
                $this->app['request']->get('timestamp'),
127
                $this->app['request']->get('nonce'),
128
            ])) {
129
            throw new BadRequestException('Invalid request signature.', 400);
130
        }
131
132
        return $this;
133
    }
134
135
    /**
136
     * Force validate request.
137
     *
138
     * @return $this
139
     */
140
    public function forceValidate()
141
    {
142
        $this->alwaysValidate = true;
143
144
        return $this;
145
    }
146
147
    /**
148
     * Get request message.
149
     *
150
     * @return array|\EasyWeChat\Kernel\Support\Collection|object|string
151
     *
152
     * @throws BadRequestException
153
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
154
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
155
     */
156
    public function getMessage()
157
    {
158
        $message = $this->parseMessage($this->app['request']->getContent(false));
159
160
        if (!is_array($message) || empty($message)) {
0 ignored issues
show
introduced by
The condition is_array($message) is always true.
Loading history...
161
            throw new BadRequestException('No message received.');
162
        }
163
164
        if ($this->isSafeMode() && !empty($message['Encrypt'])) {
165
            $this->decryptMessage($message);
166
167
            // Handle JSON format.
168
            $dataSet = json_decode($message, true);
0 ignored issues
show
Bug introduced by
$message of type array is incompatible with the type string expected by parameter $json of json_decode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

168
            $dataSet = json_decode(/** @scrutinizer ignore-type */ $message, true);
Loading history...
169
170
            if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
171
                return $dataSet;
172
            }
173
174
            $message = XML::parse($message);
0 ignored issues
show
Bug introduced by
$message of type array is incompatible with the type string expected by parameter $xml of EasyWeChat\Kernel\Support\XML::parse(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
            $message = XML::parse(/** @scrutinizer ignore-type */ $message);
Loading history...
175
        }
176
177
        return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
178
    }
179
180
    /**
181
     * Resolve server request and return the response.
182
     *
183
     * @return \Symfony\Component\HttpFoundation\Response
184
     *
185
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
186
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
187
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
188
     */
189
    protected function resolve(): Response
190
    {
191
        $result = $this->handleRequest();
192
193
        if ($this->shouldReturnRawResponse()) {
194
            return new Response($result['response']);
195
        }
196
197
        return new Response(
198
            $this->buildResponse($result['to'], $result['from'], $result['response']),
199
            200,
200
            ['Content-Type' => 'application/xml']
201
        );
202
    }
203
204
    /**
205
     * @return string|null
206
     */
207
    protected function getToken()
208
    {
209
        return $this->app['config']['token'];
210
    }
211
212
    /**
213
     * @param string                                                   $to
214
     * @param string                                                   $from
215
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
216
     *
217
     * @return string
218
     *
219
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
220
     */
221
    public function buildResponse(string $to, string $from, $message)
222
    {
223
        if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
224
            return self::SUCCESS_EMPTY_RESPONSE;
225
        }
226
227
        if ($message instanceof RawMessage) {
228
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
229
        }
230
231
        if (is_string($message) || is_numeric($message)) {
232
            $message = new Text((string) $message);
233
        }
234
235
        if (is_array($message) && reset($message) instanceof NewsItem) {
0 ignored issues
show
introduced by
The condition is_array($message) is always false.
Loading history...
236
            $message = new News($message);
237
        }
238
239
        if (!($message instanceof Message)) {
240
            throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
241
        }
242
243
        return $this->buildReply($to, $from, $message);
244
    }
245
246
    /**
247
     * Handle request.
248
     *
249
     * @return array
250
     *
251
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
252
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
253
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
254
     */
255
    protected function handleRequest(): array
256
    {
257
        $castedMessage = $this->getMessage();
258
259
        $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
260
261
        $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage);
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 140 characters; contains 145 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
262
263
        return [
264
            'to' => $messageArray['FromUserName'] ?? '',
265
            'from' => $messageArray['ToUserName'] ?? '',
266
            'response' => $response,
267
        ];
268
    }
269
270
    /**
271
     * Build reply XML.
272
     *
273
     * @param string                                        $to
274
     * @param string                                        $from
275
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
276
     *
277
     * @return string
278
     */
279
    protected function buildReply(string $to, string $from, MessageInterface $message): string
280
    {
281
        $prepends = [
282
            'ToUserName' => $to,
283
            'FromUserName' => $from,
284
            'CreateTime' => time(),
285
            'MsgType' => $message->getType(),
286
        ];
287
288
        $response = $message->transformToXml($prepends);
0 ignored issues
show
Unused Code introduced by
The call to EasyWeChat\Kernel\Contra...rface::transformToXml() has too many arguments starting with $prepends. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

288
        /** @scrutinizer ignore-call */ 
289
        $response = $message->transformToXml($prepends);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
289
290
        if ($this->isSafeMode()) {
291
            $this->app['logger']->debug('Messages safe mode is enabled.');
292
            $response = $this->app['encryptor']->encrypt($response);
293
        }
294
295
        return $response;
296
    }
297
298
    /**
299
     * @param array $params
300
     *
301
     * @return string
302
     */
303
    protected function signature(array $params)
304
    {
305
        sort($params, SORT_STRING);
306
307
        return sha1(implode($params));
308
    }
309
310
    /**
311
     * Parse message array from raw php input.
312
     *
313
     * @param string $content
314
     *
315
     * @return array
316
     *
317
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
318
     */
319
    protected function parseMessage($content)
320
    {
321
        try {
322
            if (0 === stripos($content, '<')) {
323
                $content = XML::parse($content);
324
            } else {
325
                // Handle JSON format.
326
                $dataSet = json_decode($content, true);
327
                if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
328
                    $content = $dataSet;
329
                }
330
            }
331
332
            return (array) $content;
333
        } catch (\Exception $e) {
334
            throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
335
        }
336
    }
337
338
    /**
339
     * Check the request message safe mode.
340
     *
341
     * @return bool
342
     */
343
    protected function isSafeMode(): bool
344
    {
345
        return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
346
    }
347
348
    /**
349
     * @return bool
350
     */
351
    protected function shouldReturnRawResponse(): bool
352
    {
353
        return false;
354
    }
355
356
    /**
357
     * @param array $message
358
     *
359
     * @return mixed
360
     */
361
    protected function decryptMessage(array $message)
362
    {
363
        return $message = $this->app['encryptor']->decrypt(
0 ignored issues
show
Unused Code introduced by
The assignment to $message is dead and can be removed.
Loading history...
364
            $message['Encrypt'],
365
            $this->app['request']->get('msg_signature'),
366
            $this->app['request']->get('nonce'),
367
            $this->app['request']->get('timestamp')
368
        );
369
    }
370
}
371