ServerGuard::serve()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 8
nc 1
nop 0
dl 0
loc 14
ccs 9
cts 9
cp 1
crap 1
rs 10
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;
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
    use ResponseCastable;
39
40
    /**
41
     * @var bool
42
     */
43
    protected $alwaysValidate = false;
44
45
    /**
46
     * Empty string.
47
     */
48
    public const SUCCESS_EMPTY_RESPONSE = 'success';
49
50
    /**
51
     * @var array
52
     */
53
    public const MESSAGE_TYPE_MAPPING = [
54
        'text' => Message::TEXT,
55
        'image' => Message::IMAGE,
56
        'voice' => Message::VOICE,
57
        'video' => Message::VIDEO,
58
        'shortvideo' => Message::SHORT_VIDEO,
59
        'location' => Message::LOCATION,
60
        'link' => Message::LINK,
61
        'device_event' => Message::DEVICE_EVENT,
62
        'device_text' => Message::DEVICE_TEXT,
63
        'event' => Message::EVENT,
64
        'file' => Message::FILE,
65
        'miniprogrampage' => Message::MINIPROGRAM_PAGE,
66
    ];
67
68
    /**
69
     * @var \EasyWeChat\Kernel\ServiceContainer
70
     */
71
    protected $app;
72
73
    /**
74
     * Constructor.
75
     *
76
     * @codeCoverageIgnore
77
     *
78
     * @param \EasyWeChat\Kernel\ServiceContainer $app
79
     */
80
    public function __construct(ServiceContainer $app)
81
    {
82
        $this->app = $app;
83
84
        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...
85
            call_user_func_array([$this, 'push'], $observer);
86
        }
87
    }
88
89
    /**
90
     * Handle and return response.
91
     *
92
     * @return Response
93
     *
94
     * @throws BadRequestException
95
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
96
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
97
     */
98 1
    public function serve(): Response
99
    {
100 1
        $this->app['logger']->debug('Request received:', [
101 1
            'method' => $this->app['request']->getMethod(),
102 1
            'uri' => $this->app['request']->getUri(),
103 1
            'content-type' => $this->app['request']->getContentType(),
104 1
            'content' => $this->app['request']->getContent(),
105
        ]);
106
107 1
        $response = $this->validate()->resolve();
108
109 1
        $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
110
111 1
        return $response;
112
    }
113
114
    /**
115
     * @return $this
116
     *
117
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
118
     */
119 3
    public function validate()
120
    {
121 3
        if (!$this->alwaysValidate && !$this->isSafeMode()) {
122 1
            return $this;
123
        }
124
125 2
        if ($this->app['request']->get('signature') !== $this->signature([
126 2
                $this->getToken(),
127 2
                $this->app['request']->get('timestamp'),
128 2
                $this->app['request']->get('nonce'),
129
            ])) {
130 1
            throw new BadRequestException('Invalid request signature.', 400);
131
        }
132
133 1
        return $this;
134
    }
135
136
    /**
137
     * Force validate request.
138
     *
139
     * @return $this
140
     */
141 1
    public function forceValidate()
142
    {
143 1
        $this->alwaysValidate = true;
144
145 1
        return $this;
146
    }
147
148
    /**
149
     * Get request message.
150
     *
151
     * @return array|\EasyWeChat\Kernel\Support\Collection|object|string
152
     *
153
     * @throws BadRequestException
154
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
155
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
156
     */
157 5
    public function getMessage()
158
    {
159 5
        $message = $this->parseMessage($this->app['request']->getContent(false));
160
161 5
        if (!is_array($message) || empty($message)) {
0 ignored issues
show
introduced by
The condition is_array($message) is always true.
Loading history...
162 1
            throw new BadRequestException('No message received.');
163
        }
164
165 4
        if ($this->isSafeMode() && !empty($message['Encrypt'])) {
166 1
            $message = $this->decryptMessage($message);
167
168
            // Handle JSON format.
169 1
            $dataSet = json_decode($message, true);
170
171 1
            if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
172 1
                return $dataSet;
173
            }
174
175 1
            $message = XML::parse($message);
176
        }
177
178 4
        return $this->detectAndCastResponseToType($message, $this->app->config->get('response_type'));
179
    }
180
181
    /**
182
     * Resolve server request and return the response.
183
     *
184
     * @return \Symfony\Component\HttpFoundation\Response
185
     *
186
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
187
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
188
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidConfigException
189
     */
190 2
    protected function resolve(): Response
191
    {
192 2
        $result = $this->handleRequest();
193
194 2
        if ($this->shouldReturnRawResponse()) {
195 1
            $response = new Response($result['response']);
196
        } else {
197 1
            $response = new Response(
198 1
                $this->buildResponse($result['to'], $result['from'], $result['response']),
199 1
                200,
200 1
                ['Content-Type' => 'application/xml']
201
            );
202
        }
203
204 2
        $this->app->events->dispatch(new Events\ServerGuardResponseCreated($response));
205
206 2
        return $response;
207
    }
208
209
    /**
210
     * @return string|null
211
     */
212 2
    protected function getToken()
213
    {
214 2
        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 1
    public function buildResponse(string $to, string $from, $message)
227
    {
228 1
        if (empty($message) || self::SUCCESS_EMPTY_RESPONSE === $message) {
229 1
            return self::SUCCESS_EMPTY_RESPONSE;
230
        }
231
232 1
        if ($message instanceof RawMessage) {
233 1
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
234
        }
235
236 1
        if (is_string($message) || is_numeric($message)) {
237 1
            $message = new Text((string) $message);
238
        }
239
240 1
        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 1
            $message = new News($message);
242
        }
243
244 1
        if (!($message instanceof Message)) {
245 1
            throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
246
        }
247
248 1
        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 1
    protected function handleRequest(): array
261
    {
262 1
        $castedMessage = $this->getMessage();
263
264 1
        $messageArray = $this->detectAndCastResponseToType($castedMessage, 'array');
265
266 1
        $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$messageArray['MsgType'] ?? $messageArray['msg_type'] ?? 'text'], $castedMessage);
267
268
        return [
269 1
            'to' => $messageArray['FromUserName'] ?? '',
270 1
            'from' => $messageArray['ToUserName'] ?? '',
271 1
            '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 1
    protected function buildReply(string $to, string $from, MessageInterface $message): string
285
    {
286
        $prepends = [
287 1
            'ToUserName' => $to,
288 1
            'FromUserName' => $from,
289 1
            'CreateTime' => time(),
290 1
            'MsgType' => $message->getType(),
291
        ];
292
293 1
        $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 1
        if ($this->isSafeMode()) {
296 1
            $this->app['logger']->debug('Messages safe mode is enabled.');
297 1
            $response = $this->app['encryptor']->encrypt($response);
298
        }
299
300 1
        return $response;
301
    }
302
303
    /**
304
     * @param array $params
305
     *
306
     * @return string
307
     */
308 2
    protected function signature(array $params)
309
    {
310 2
        sort($params, SORT_STRING);
311
312 2
        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 7
    protected function parseMessage($content)
325
    {
326
        try {
327 7
            if (0 === stripos($content, '<')) {
328 6
                $content = XML::parse($content);
329
            } else {
330
                // Handle JSON format.
331 2
                $dataSet = json_decode($content, true);
332 2
                if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
333 1
                    $content = $dataSet;
334
                }
335
            }
336
337 6
            return (array) $content;
338 1
        } catch (\Exception $e) {
339 1
            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 9
    protected function isSafeMode(): bool
349
    {
350 9
        return $this->app['request']->get('signature') && 'aes' === $this->app['request']->get('encrypt_type');
351
    }
352
353
    /**
354
     * @return bool
355
     */
356 1
    protected function shouldReturnRawResponse(): bool
357
    {
358 1
        return false;
359
    }
360
361
    /**
362
     * @param array $message
363
     *
364
     * @return mixed
365
     */
366 1
    protected function decryptMessage(array $message)
367
    {
368 1
        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...
369 1
            $message['Encrypt'],
370 1
            $this->app['request']->get('msg_signature'),
371 1
            $this->app['request']->get('nonce'),
372 1
            $this->app['request']->get('timestamp')
373
        );
374
    }
375
}
376