Completed
Push — master ( 50e1b8...d16d3d )
by Carlos
02:37
created

ServerGuard::buildResponse()   C

Complexity

Conditions 9
Paths 10

Size

Total Lines 23
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 11
nc 10
nop 3
dl 0
loc 23
ccs 0
cts 10
cp 0
crap 90
rs 5.8541
c 0
b 0
f 0
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->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
    public function forceValidate()
139
    {
140
        $this->alwaysValidate = true;
141
142
        return $this;
143
    }
144
145
    /**
146
     * Get request message.
147
     *
148
     * @return array|\EasyWeChat\Kernel\Support\Collection|object|string
149
     *
150
     * @throws BadRequestException
151
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
152
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
153
     */
154
    public function getMessage()
155
    {
156
        $message = $this->parseMessage($this->app['request']->getContent(false));
157
158
        if (!is_array($message) || empty($message)) {
0 ignored issues
show
introduced by
The condition is_array($message) is always true.
Loading history...
159
            throw new BadRequestException('No message received.');
160
        }
161
162
        if ($this->isSafeMode() && !empty($message['Encrypt'])) {
163
            $message = $this->app['encryptor']->decrypt(
164
                $message['Encrypt'],
165
                $this->app['request']->get('msg_signature'),
166
                $this->app['request']->get('nonce'),
167
                $this->app['request']->get('timestamp')
168
            );
169
170
            // Handle JSON format.
171
            $dataSet = json_decode($message, true);
172
173
            if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
174
                return $dataSet;
175
            }
176
177
            $message = XML::parse($message);
178
        }
179
180
        return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
181
    }
182
183
    /**
184
     * Resolve server request and return the response.
185
     *
186
     * @return \Symfony\Component\HttpFoundation\Response
187
     *
188
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
189
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
190
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
191
     */
192
    protected function resolve(): Response
193
    {
194
        $result = $this->handleRequest();
195
196
        if ($this->shouldReturnRawResponse()) {
197
            return new Response($result['response']);
198
        }
199
200
        return new Response(
201
            $this->buildResponse($result['to'], $result['from'], $result['response']),
202
            200,
203
            ['Content-Type' => 'application/xml']
204
        );
205
    }
206
207
    /**
208
     * @return string|null
209
     */
210
    protected function getToken()
211
    {
212
        return $this->app['config']['token'];
213
    }
214
215
    /**
216
     * @param string                                                   $to
217
     * @param string                                                   $from
218
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
219
     *
220
     * @return string
221
     *
222
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
223
     */
224
    public function buildResponse(string $to, string $from, $message)
225
    {
226
        if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
227
            return self::SUCCESS_EMPTY_RESPONSE;
228
        }
229
230
        if ($message instanceof RawMessage) {
231
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
232
        }
233
234
        if (is_string($message) || is_numeric($message)) {
235
            $message = new Text((string) $message);
236
        }
237
238
        if (is_array($message) && reset($message) instanceof NewsItem) {
0 ignored issues
show
introduced by
The condition is_array($message) is always false.
Loading history...
239
            $message = new News($message);
240
        }
241
242
        if (!($message instanceof Message)) {
243
            throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
244
        }
245
246
        return $this->buildReply($to, $from, $message);
247
    }
248
249
    /**
250
     * Handle request.
251
     *
252
     * @return array
253
     *
254
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
255
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
256
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
257
     */
258
    protected function handleRequest(): array
259
    {
260
        $castedMessage = $this->getMessage();
261
262
        $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
263
264
        $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...
265
266
        return [
267
            'to' => $messageArray['FromUserName'] ?? '',
268
            'from' => $messageArray['ToUserName'] ?? '',
269
            'response' => $response,
270
        ];
271
    }
272
273
    /**
274
     * Build reply XML.
275
     *
276
     * @param string                                        $to
277
     * @param string                                        $from
278
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
279
     *
280
     * @return string
281
     */
282
    protected function buildReply(string $to, string $from, MessageInterface $message): string
283
    {
284
        $prepends = [
285
            'ToUserName' => $to,
286
            'FromUserName' => $from,
287
            'CreateTime' => time(),
288
            'MsgType' => $message->getType(),
289
        ];
290
291
        $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

291
        /** @scrutinizer ignore-call */ 
292
        $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...
292
293
        if ($this->isSafeMode()) {
294
            $this->app['logger']->debug('Messages safe mode is enabled.');
295
            $response = $this->app['encryptor']->encrypt($response);
296
        }
297
298
        return $response;
299
    }
300
301
    /**
302
     * @param array $params
303
     *
304
     * @return string
305
     */
306
    protected function signature(array $params)
307
    {
308
        sort($params, SORT_STRING);
309
310
        return sha1(implode($params));
311
    }
312
313
    /**
314
     * Parse message array from raw php input.
315
     *
316
     * @param string $content
317
     *
318
     * @return array
319
     *
320
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
321
     */
322
    protected function parseMessage($content)
323
    {
324
        try {
325
            if (0 === stripos($content, '<')) {
326
                $content = XML::parse($content);
327
            } else {
328
                // Handle JSON format.
329
                $dataSet = json_decode($content, true);
330
                if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
331
                    $content = $dataSet;
332
                }
333
            }
334
335
            return (array) $content;
336
        } catch (\Exception $e) {
337
            throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
338
        }
339
    }
340
341
    /**
342
     * Check the request message safe mode.
343
     *
344
     * @return bool
345
     */
346
    protected function isSafeMode(): bool
347
    {
348
        if ($this->alwaysValidate) {
349
            return true;
350
        }
351
352
        return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
353
    }
354
355
    /**
356
     * @return bool
357
     */
358
    protected function shouldReturnRawResponse(): bool
359
    {
360
        return false;
361
    }
362
}
363