Passed
Pull Request — master (#1252)
by Keal
02:21
created

ServerGuard::shouldReturnRawResponse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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