Passed
Push — master ( 40a49e...c9be10 )
by William
02:50
created

Report::setTimestamp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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