1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Ecodev\Felix\Log; |
||
6 | |||
7 | use Ecodev\Felix\Model\CurrentUser; |
||
8 | use Laminas\Log\Processor\ProcessorInterface; |
||
9 | |||
10 | class EventCompleter implements ProcessorInterface |
||
11 | { |
||
12 | 5 | public function __construct(private readonly string $baseUrl) |
|
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
13 | { |
||
14 | 5 | } |
|
15 | |||
16 | /** |
||
17 | * Complete a log event with extra data, including stacktrace and any global stuff relevant to the app. |
||
18 | */ |
||
19 | 4 | public function process(array $event): array |
|
20 | { |
||
21 | 4 | $envData = $this->getEnvData(); |
|
22 | 4 | $event = array_merge($event, $envData); |
|
23 | |||
24 | // If we are logging PHP errors, then we include all known information in message |
||
25 | 4 | if ($event['extra']['errno'] ?? false) { |
|
26 | 1 | $event['message'] .= "\nStacktrace:\n" . $this->getStacktrace(); |
|
27 | } |
||
28 | |||
29 | // Security hide clear text password |
||
30 | 4 | unset($event['extra']['password']); |
|
31 | |||
32 | 4 | return $event; |
|
33 | } |
||
34 | |||
35 | /** |
||
36 | * Retrieve dynamic information from environment to be logged. |
||
37 | */ |
||
38 | 4 | private function getEnvData(): array |
|
39 | { |
||
40 | 4 | $user = CurrentUser::get(); |
|
41 | |||
42 | 4 | if (PHP_SAPI === 'cli') { |
|
43 | global $argv; |
||
44 | 4 | $request = $argv; |
|
45 | 4 | $ip = !empty(getenv('REMOTE_ADDR')) ? getenv('REMOTE_ADDR') : 'script'; |
|
46 | 4 | $url = implode(' ', $argv); |
|
47 | 4 | $referer = ''; |
|
48 | } else { |
||
49 | $request = $_REQUEST; |
||
50 | $ip = $_SERVER['REMOTE_ADDR'] ?? ''; |
||
51 | $url = $this->baseUrl . $_SERVER['REQUEST_URI']; |
||
52 | $referer = $_SERVER['HTTP_REFERER'] ?? ''; |
||
53 | } |
||
54 | |||
55 | 4 | $request = $this->removeSensitiveData($request); |
|
56 | |||
57 | 4 | $envData = [ |
|
58 | 4 | 'creator_id' => $user ? $user->getId() : null, |
|
59 | 4 | 'login' => $user ? $user->getLogin() : null, |
|
60 | 4 | 'url' => $url, |
|
61 | 4 | 'referer' => $referer, |
|
62 | 4 | 'request' => json_encode($request, JSON_PRETTY_PRINT), |
|
63 | 4 | 'ip' => $ip, |
|
64 | ]; |
||
65 | |||
66 | 4 | return $envData; |
|
67 | } |
||
68 | |||
69 | /** |
||
70 | * Remove password value from GraphQL variables well-known structure. |
||
71 | */ |
||
72 | 4 | protected function removeSensitiveData(array $request): array |
|
73 | { |
||
74 | 4 | foreach ($request as &$r) { |
|
75 | 4 | if (is_array($r)) { |
|
76 | unset($r['variables']['password']); |
||
77 | } |
||
78 | } |
||
79 | |||
80 | 4 | return $request; |
|
81 | } |
||
82 | |||
83 | /** |
||
84 | * Returns the backtrace excluding the most recent calls to this function so we only get the interesting parts. |
||
85 | */ |
||
86 | 1 | private function getStacktrace(): string |
|
87 | { |
||
88 | 1 | ob_start(); |
|
89 | 1 | @debug_print_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); |
|
90 | 1 | $trace = ob_get_contents(); |
|
91 | 1 | ob_end_clean(); |
|
92 | |||
93 | 1 | if ($trace === false) { |
|
94 | return 'Could not get stacktrace'; |
||
95 | } |
||
96 | |||
97 | // Remove first items from backtrace as it's this function and previous logging functions which is not interesting |
||
98 | 1 | $shortenTrace = preg_replace('/^#[0-4]\s+[^\n]*\n/m', '', $trace); |
|
99 | |||
100 | 1 | if ($shortenTrace === null) { |
|
101 | return $trace; |
||
102 | } |
||
103 | |||
104 | // Renumber backtrace items. |
||
105 | 1 | $renumberedTrace = preg_replace_callback('/^#(\d+)/m', fn ($matches) => '#' . ((int) $matches[1] - 5), $shortenTrace); |
|
106 | |||
107 | 1 | if ($renumberedTrace === null) { |
|
108 | return $shortenTrace; |
||
109 | } |
||
110 | |||
111 | 1 | return $renumberedTrace; |
|
112 | } |
||
113 | } |
||
114 |