Passed
Push — master ( a2a2c3...5dc036 )
by Carlos
03:01
created

ServerGuard::buildResponse()   C

Complexity

Conditions 9
Paths 10

Size

Total Lines 24
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
cc 9
eloc 12
nc 10
nop 3
dl 0
loc 24
ccs 0
cts 19
cp 0
crap 90
rs 5.3563
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 Symfony\Component\HttpFoundation\Response;
25
26
/**
27
 * Class ServerGuard.
28
 *
29
 * 1. url 里的 signature 只是将 token+nonce+timestamp 得到的签名,只是用于验证当前请求的,在公众号环境下一直有
30
 * 2. 企业号消息发送时是没有的,因为固定为完全模式,所以 url 里不会存在 signature, 只有 msg_signature 用于解密消息的
31
 *
32
 * @author overtrue <[email protected]>
33
 */
34
class ServerGuard
35
{
36
    use Observable;
37
38
    /**
39
     * @var bool
40
     */
41
    protected $alwaysValidate = false;
42
43
    /**
44
     * Empty string.
45
     */
46
    const SUCCESS_EMPTY_RESPONSE = 'success';
47
48
    /**
49
     * @var array
50
     */
51
    const MESSAGE_TYPE_MAPPING = [
52
        'text' => Message::TEXT,
53
        'image' => Message::IMAGE,
54
        'voice' => Message::VOICE,
55
        'video' => Message::VIDEO,
56
        'shortvideo' => Message::SHORT_VIDEO,
57
        'location' => Message::LOCATION,
58
        'link' => Message::LINK,
59
        'device_event' => Message::DEVICE_EVENT,
60
        'device_text' => Message::DEVICE_TEXT,
61
        'event' => Message::EVENT,
62
        'file' => Message::FILE,
63
    ];
64
65
    /**
66
     * @var \EasyWeChat\Kernel\ServiceContainer
67
     */
68
    protected $app;
69
70
    /**
71
     * Constructor.
72
     *
73
     * @param \EasyWeChat\Kernel\ServiceContainer $app
74
     */
75
    public function __construct(ServiceContainer $app)
76
    {
77
        $this->app = $app;
78
    }
79
80
    /**
81
     * Handle and return response.
82
     *
83
     * @return Response
84
     *
85
     * @throws BadRequestException
86
     */
87
    public function serve(): Response
88
    {
89
        $this->app['logger']->debug('Request received:', [
90
            'method' => $this->app['request']->getMethod(),
91
            'uri' => $this->app['request']->getUri(),
92
            'content-type' => $this->app['request']->getContentType(),
93
            'content' => $this->app['request']->getContent(),
94
        ]);
95
96
        $response = $this->validate()->resolve();
97
98
        $this->app['logger']->debug('Server response created:', ['content' => $response->getContent()]);
99
100
        return $response;
101
    }
102
103
    /**
104
     * @return $this
105
     *
106
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
107
     */
108
    public function validate()
109
    {
110
        if (!$this->isSafeMode()) {
111
            return $this;
112
        }
113
114
        if ($this->app['request']->get('signature') !== $this->signature([
115
                $this->getToken(),
116
                $this->app['request']->get('timestamp'),
117
                $this->app['request']->get('nonce'),
118
            ])) {
119
            throw new BadRequestException('Invalid request signature.', 400);
120
        }
121
122
        return $this;
123
    }
124
125
    /**
126
     * Get request message.
127
     *
128
     * @return array
129
     *
130
     * @throws BadRequestException
131
     */
132
    public function getMessage()
133
    {
134
        $message = $this->parseMessage($this->app['request']->getContent(false));
135
136
        if (!is_array($message) || empty($message)) {
137
            throw new BadRequestException('No message received.');
138
        }
139
140
        if ($this->isSafeMode() && !empty($message['Encrypt'])) {
141
            $message = $this->app['encryptor']->decrypt(
142
                $message['Encrypt'],
143
                $this->app['request']->get('msg_signature'),
144
                $this->app['request']->get('nonce'),
145
                $this->app['request']->get('timestamp')
146
            );
147
148
            // Handle JSON format.
149
            $dataSet = json_decode($message, true);
150
            if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
151
                return $dataSet;
152
            }
153
154
            return XML::parse($message);
155
        }
156
157
        return $message;
158
    }
159
160
    /**
161
     * Resolve server request and return the response.
162
     *
163
     * @return \Symfony\Component\HttpFoundation\Response
164
     */
165
    protected function resolve(): Response
166
    {
167
        $result = $this->handleRequest();
168
169
        if ($this->shouldReturnRawResponse()) {
170
            return new Response($result['response']);
171
        }
172
173
        return new Response(
174
            $this->buildResponse($result['to'], $result['from'], $result['response']),
175
            200,
176
            ['Content-Type' => 'application/xml']
177
        );
178
    }
179
180
    /**
181
     * @return string|null
182
     */
183
    protected function getToken()
184
    {
185
        return $this->app['config']['token'];
186
    }
187
188
    /**
189
     * @param string                                                   $to
190
     * @param string                                                   $from
191
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface|string|int $message
192
     *
193
     * @return mixed|string
194
     *
195
     * @throws \EasyWeChat\Kernel\Exceptions\InvalidArgumentException
196
     */
197
    public function buildResponse(string $to, string $from, $message)
198
    {
199
        if (empty($message) || $message === self::SUCCESS_EMPTY_RESPONSE) {
200
            return self::SUCCESS_EMPTY_RESPONSE;
201
        }
202
203
        if ($message instanceof RawMessage) {
204
            return $message->get('content', self::SUCCESS_EMPTY_RESPONSE);
205
        }
206
207
        if (is_string($message) || is_numeric($message)) {
208
            $message = new Text((string) $message);
209
        }
210
211
        if (is_array($message) && reset($message) instanceof NewsItem) {
212
            $message = new News($message);
213
        }
214
215
        if (!($message instanceof Message)) {
216
            throw new InvalidArgumentException(sprintf('Invalid Messages type "%s".', gettype($message)));
217
        }
218
219
        return $this->buildReply($to, $from, $message);
220
    }
221
222
    /**
223
     * Handle request.
224
     *
225
     * @return array
226
     *
227
     * @throws \EasyWeChat\Kernel\Exceptions\RuntimeException
228
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
229
     */
230
    protected function handleRequest(): array
231
    {
232
        $message = $this->getMessage();
233
234
        $response = $this->dispatch(self::MESSAGE_TYPE_MAPPING[$message['MsgType'] ?? 'text'], $message);
235
236
        return [
237
            'to' => $message['FromUserName'] ?? '',
238
            'from' => $message['ToUserName'] ?? '',
239
            'response' => $response,
240
        ];
241
    }
242
243
    /**
244
     * Build reply XML.
245
     *
246
     * @param string                                        $to
247
     * @param string                                        $from
248
     * @param \EasyWeChat\Kernel\Contracts\MessageInterface $message
249
     *
250
     * @return string
251
     */
252
    protected function buildReply(string $to, string $from, MessageInterface $message): string
253
    {
254
        $prepends = [
255
            'ToUserName' => $to,
256
            'FromUserName' => $from,
257
            'CreateTime' => time(),
258
            'MsgType' => $message->getType(),
259
        ];
260
261
        $response = $message->transformToXml($prepends);
262
263
        if ($this->isSafeMode()) {
264
            $this->app['logger']->debug('Messages safe mode is enabled.');
265
            $response = $this->app['encryptor']->encrypt($response);
266
        }
267
268
        return $response;
269
    }
270
271
    /**
272
     * @param array $params
273
     *
274
     * @return string
275
     */
276
    protected function signature(array $params)
277
    {
278
        sort($params, SORT_STRING);
279
280
        return sha1(implode($params));
281
    }
282
283
    /**
284
     * Parse message array from raw php input.
285
     *
286
     * @param string|resource $content
287
     *
288
     * @return array
289
     *
290
     * @throws \EasyWeChat\Kernel\Exceptions\BadRequestException
291
     */
292
    protected function parseMessage($content)
293
    {
294
        try {
295
            if (stripos($content, '<') === 0) {
296
                $content = XML::parse($content);
0 ignored issues
show
Bug introduced by
It seems like $content defined by \EasyWeChat\Kernel\Support\XML::parse($content) on line 296 can also be of type resource; however, EasyWeChat\Kernel\Support\XML::parse() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
297
            } else {
298
                // Handle JSON format.
299
                $dataSet = json_decode($content, true);
300
                if ($dataSet && (JSON_ERROR_NONE === json_last_error())) {
301
                    $content = $dataSet;
302
                }
303
            }
304
305
            return (array) $content;
306
        } catch (\Exception $e) {
307
            throw new BadRequestException(sprintf('Invalid message content:(%s) %s', $e->getCode(), $e->getMessage()), $e->getCode());
308
        }
309
    }
310
311
    /**
312
     * Check the request message safe mode.
313
     *
314
     * @return bool
315
     */
316
    protected function isSafeMode(): bool
317
    {
318
        if ($this->alwaysValidate) {
319
            return true;
320
        }
321
322
        return $this->app['request']->get('signature') && $this->app['request']->get('encrypt_type') === 'aes';
323
    }
324
325
    /**
326
     * @return bool
327
     */
328
    protected function shouldReturnRawResponse(): bool
329
    {
330
        return false;
331
    }
332
}
333