Passed
Push — master ( 627320...920c75 )
by William
04:16
created

Report::getExceptionJS()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 20
c 1
b 0
f 0
nc 2
nop 0
dl 0
loc 26
ccs 19
cts 19
cp 1
crap 2
rs 9.6
1
<?php
2
3
namespace App;
4
5
use stdClass;
6
7
use function array_keys;
8
use function array_merge;
9
use function bin2hex;
10
use function date;
11
use function html_entity_decode;
12
use function htmlspecialchars_decode;
13
use function json_decode;
14
use function openssl_random_pseudo_bytes;
15
16
use const ENT_HTML5;
17
use const ENT_QUOTES;
18
19
/**
20
 * Represents an user report
21
 */
22
class Report extends stdClass
23
{
24
    /** @var string */
25
    private $internal____date = null;
26
27
    /** @var string */
28
    private $internal____eventId = null;
29
30
    /** @var string */
31
    private $internal____userMessage = null;
32
33 7
    public function hasUserFeedback(): bool
34
    {
35 7
        return $this->internal____userMessage !== null;
36
    }
37
38 7
    public function getUserFeedback(): string
39
    {
40 7
        return $this->internal____userMessage ?? '';
41
    }
42
43 14
    public static function fromString(string $input): Report
44
    {
45 14
        $obj = json_decode($input);
46
47 14
        return self::fromObject((object) $obj);
48
    }
49
50 14
    public static function fromObject(stdClass $input): Report
51
    {
52 14
        $obj = (object) $input;
53 14
        $keys = array_keys((array) $obj);
54 14
        $report = new Report();
55 14
        foreach ($keys as $propertyName) {
56 14
            if ($propertyName === 'steps') {
57 14
                $report->internal____userMessage = $obj->{$propertyName};
58
            } else {
59 14
                $report->{$propertyName} = $obj->{$propertyName};
60
            }
61
        }
62
63 14
        return $report;
64
    }
65
66
    public function setTimestamp(string $timestamp): void
67
    {
68
        $this->internal____date = $timestamp;
69
    }
70
71 7
    public function getEventId(): string
72
    {
73 7
        if ($this->internal____eventId === null) {
74 7
            $this->internal____eventId = bin2hex((string) openssl_random_pseudo_bytes(16));
75
        }
76
77 7
        return $this->internal____eventId;
78
    }
79
80 7
    public function getTimestampUTC(): string
81
    {
82 7
        return $this->internal____date ?? date('Y-m-d\TH:i:s');
83
    }
84
85 7
    public function getTags(): stdClass
86
    {
87
        /*
88
            "pma_version": "4.8.6-dev",
89
            "browser_name": "CHROME",
90
            "browser_version": "72.0.3626.122",
91
            "user_os": "Linux",
92
            "server_software": "nginx/1.14.0",
93
            "user_agent_string": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.122 Safari/537.36 Vivaldi/2.3.1440.61",
94
            "locale": "fr",
95
            "configuration_storage": "enabled",
96
            "php_version": "7.2.16-1+ubuntu18.04.1+deb.sury.org+1",
97
            "exception_type": "php",
98
        */
99 7
        $tags = new stdClass();
100
        //$tags->pma_version = $this->{'pma_version'} ?? null;
101
        //$tags->browser_name = $this->{'browser_name'} ?? null;
102
        //$tags->browser_version = $this->{'browser_version'} ?? null;
103
        //$tags->user_os = $this->{'user_os'} ?? null;
104 7
        $tags->server_software = $this->{'server_software'} ?? null;
105 7
        $tags->user_agent_string = $this->{'user_agent_string'} ?? null;
106 7
        $tags->locale = $this->{'locale'} ?? null;
107 7
        $tags->configuration_storage = $this->{'configuration_storage'} === 'enabled'; // "enabled" or "disabled"
108 7
        $tags->php_version = $this->{'php_version'} ?? null;
109 7
        $tags->exception_type = $this->{'exception_type'} ?? null;// js or php
110
111 7
        return $tags;
112
    }
113
114 7
    public function getContexts(): stdClass
115
    {
116 7
        $contexts = new stdClass();
117 7
        $contexts->os = new stdClass();
118 7
        $contexts->os->name = $this->{'user_os'} ?? null;
119
120 7
        $contexts->browser = new stdClass();
121 7
        $contexts->browser->name = $this->{'browser_name'} ?? null;
122 7
        $contexts->browser->version = $this->{'browser_version'} ?? null;
123
124 7
        return $contexts;
125
    }
126
127 7
    public function decode(string $text): string
128
    {
129 7
        return htmlspecialchars_decode(html_entity_decode($text, ENT_QUOTES | ENT_HTML5));
130
    }
131
132
    /**
133
     * @return array<string,mixed>
134
     */
135 7
    public function getExceptionJS(): array
136
    {
137 7
        $exception = new stdClass();
138 7
        $exception->type = $this->decode($this->{'exception'}->name ?? '');
139 7
        $exception->value = $this->decode($this->{'exception'}->message ?? '');
140 7
        $exception->stacktrace = new stdClass();
141 7
        $exception->stacktrace->frames = [];
142 7
        $exStack = ($this->{'exception'} ?? (object) ['stack' => []])->{'stack'} ?? [];
143 7
        foreach ($exStack as $stack) {
144 7
            $exception->stacktrace->frames[] = [
145 7
                'platform' => 'javascript',
146 7
                'function' => $this->decode($stack->{'func'} ?? ''),
147 7
                'lineno' => (int) ($stack->{'line'} ?? 0),
148 7
                'colno' => (int) ($stack->{'column'} ?? 0),
149 7
                'abs_path' => $stack->{'uri'} ?? '',
150 7
                'filename' => $stack->{'scriptname'} ?? '',
151
            ];
152
        }
153
154
        return [
155 7
            'platform' => 'javascript',
156
            'exception' => [
157 7
                'values' => [$exception],
158
            ],
159 7
            'message' => $this->decode($this->{'exception'}->message ?? ''),
160 7
            'culprit' => $this->{'script_name'},
161
        ];
162
    }
163
164 7
    public function getExtras(): stdClass
165
    {
166 7
        return new stdClass();
167
    }
168
169 7
    public function getUserMessage(): stdClass
170
    {
171 7
        $userMessage = new stdClass();
172 7
        $userMessage->{'message'} = $this->decode($this->{'description'} ?? '');
173
174 7
        return $userMessage;
175
    }
176
177 7
    public function getUser(): stdClass
178
    {
179 7
        $user = new stdClass();
180 7
        $user->ip_address = '0.0.0.0';
181
182 7
        return $user;
183
    }
184
185 7
    public function isMultiReports(): bool
186
    {
187 7
        return isset($this->{'exception_type'}) && $this->{'exception_type'} === 'php';
188
    }
189
190
    public function typeToLevel(string $type): string
191
    {
192
        switch ($type) {
193
            case 'Internal error':
194
            case 'Parsing Error':
195
            case 'Error':
196
            case 'Core Error':
197
                return 'error';
198
199
            case 'User Error':
200
            case 'User Warning':
201
            case 'User Notice':
202
                return 'info';
203
204
            case 'Warning':
205
            case 'Runtime Notice':
206
            case 'Deprecation Notice':
207
            case 'Notice':
208
            case 'Compile Warning':
209
                return 'warning';
210
211
            case 'Catchable Fatal Error':
212
                return 'tatal';
213
214
            default:
215
                return 'error';
216
        }
217
    }
218
219
    /**
220
     * @return array<int,array<string,mixed>>
221
     */
222
    public function getMultiDataToSend(): array
223
    {
224
        $reports = [];
225
        /*
226
        {
227
            "lineNum": 272,
228
            "file": "./libraries/classes/Plugins/Export/ExportXml.php",
229
            "type": "Warning",
230
            "msg": "count(): Parameter must be an array or an object that implements Countable",
231
            "stackTrace": [
232
                {
233
                "file": "./libraries/classes/Plugins/Export/ExportXml.php",
234
                "line": 272,
235
                "function": "count",
236
                "args": [
237
                    "NULL"
238
                ]
239
                },
240
                {
241
                "file": "./export.php",
242
                "line": 415,
243
                "function": "exportHeader",
244
                "class": "PhpMyAdmin\\Plugins\\Export\\ExportXml",
245
                "type": "->"
246
                }
247
            ],
248
            "stackhash": "e6e0b1e1b9d90fee08a5ab8226e485fb"
249
        }
250
        */
251
        foreach ($this->{'errors'} as $error) {
252
            $exception = new stdClass();
253
            $exception->type = $this->decode($error->{'type'} ?? '');
254
            $exception->value = $this->decode($error->{'msg'} ?? '');
255
            $exception->stacktrace = new stdClass();
256
            $exception->stacktrace->frames = [];
257
258
            foreach ($error->{'stackTrace'} as $stack) {
259
                $trace = [
260
                    'platform' => 'php',
261
                    'function' => $stack->{'function'},
262
                    'lineno' => (int) $stack->{'line'},
263
                    'filename' => $error->{'file'},
264
                ];
265
                if (isset($stack->{'class'})) {
266
                    $trace['package'] = $stack->{'class'};
267
                }
268
269
                if (isset($stack->{'type'})) {
270
                    $trace['symbol_addr'] = $stack->{'type'};
271
                }
272
273
                if (isset($stack->{'args'})) {// function arguments
274
                    $trace['vars'] = (object) $stack->{'args'};
275
                }
276
277
                $exception->stacktrace->frames[] = $trace;
278
            }
279
280
            $reports[] = [
281
                'platform' => 'php',
282
                'level' => $this->typeToLevel($error->{'type'}),
283
                'exception' => [
284
                    'values' => [$exception],
285
                ],
286
                'message' => $this->decode($error->{'msg'} ?? ''),
287
                'culprit' => $error->{'file'},
288
            ];
289
        }
290
291
        return $reports;
292
    }
293
294
    /**
295
     * @return array<string,mixed>
296
     */
297 7
    public function toJson(): array
298
    {
299 7
        $exType = $this->{'exception_type'} ?? 'js';
300
301
        return [
302 7
            'sentry.interfaces.Message' => $this->getUserMessage(),
303 7
            'release' => $this->{'pma_version'},
304 7
            'platform' => $exType === 'js' ? 'javascript' : 'php',
305 7
            'timestamp' => $this->getTimestampUTC(),
306 7
            'tags' => $this->getTags(),
307 7
            'extra' => $this->getExtras(),
308 7
            'contexts' => $this->getContexts(),
309 7
            'user' => $this->getUser(),
310
            //TODO: 'environment'
311
            //TODO: 'level'
312
        ];
313
    }
314
315
    /**
316
     * @return array<int,array<string,mixed>>
317
     */
318 7
    public function getReports(): array
319
    {
320 7
        if ($this->isMultiReports()) {
321
            $reports = [];
322
            foreach ($this->getMultiDataToSend() as $data) {
323
                $reports[] = array_merge($this->toJson(), $data, [
324
                    'event_id' => $this->getEventId(),
325
                ]);
326
            }
327
328
            return $reports;
329
        }
330
331
        return [
332 7
            array_merge(
333 7
                $this->toJson(),
334 7
                $this->getExceptionJs(),
335
                [
336 7
                    'event_id' => $this->getEventId(),
337
                ]
338
            ),
339
        ];
340
    }
341
}
342