Passed
Push — master ( b13c48...ab54be )
by William
21:16 queued 01:18
created

Report::getUserId()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1.0028

Importance

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