Subversion Repositories oidplus

Rev

Rev 1316 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
115 daniel-mar 1
<?php
2
 
3
/*
4
 * OIDplus 2.0
1086 daniel-mar 5
 * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
115 daniel-mar 6
 *
7
 * Licensed under the Apache License, Version 2.0 (the "License");
8
 * you may not use this file except in compliance with the License.
9
 * You may obtain a copy of the License at
10
 *
11
 *     http://www.apache.org/licenses/LICENSE-2.0
12
 *
13
 * Unless required by applicable law or agreed to in writing, software
14
 * distributed under the License is distributed on an "AS IS" BASIS,
15
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
 * See the License for the specific language governing permissions and
17
 * limitations under the License.
18
 */
19
 
1050 daniel-mar 20
namespace ViaThinkSoft\OIDplus;
511 daniel-mar 21
 
1086 daniel-mar 22
// phpcs:disable PSR1.Files.SideEffects
23
\defined('INSIDE_OIDPLUS') or die;
24
// phpcs:enable PSR1.Files.SideEffects
25
 
730 daniel-mar 26
class OIDplusLogger extends OIDplusBaseClass {
115 daniel-mar 27
 
1116 daniel-mar 28
        /**
1267 daniel-mar 29
         * This method splits a mask code containing multiple components (delimited by '+') into single components
30
         * It takes care that '+' inside brackets isn't be used to split the codes
1116 daniel-mar 31
         * Also, brackets can be escaped.
32
         * The severity block (optional, must be standing in front of a component)
33
         * is handled too. Inside the severity block, you may only use '/' to split components.
34
         * The severity block will be implicitly repeated from the previous components if a component
35
         * does not feature one.
1267 daniel-mar 36
         * @param string $maskcode A maskcode, e.g. [INFO]OID(2.999)
37
         * @return array|false An array of [$severity,$target],
38
         * where $severity is 'INFO' or [$online,$offline] like ['INFO','INFO']
39
         * and $target is like ['A'], ['OID', '2.999'], etc.
1116 daniel-mar 40
         */
1267 daniel-mar 41
        public static function parse_maskcode(string $maskcode) {
188 daniel-mar 42
                $out = array();
288 daniel-mar 43
                $sevs = array(); // Note: The severity block will repeat for the next components if not changed explicitly
289 daniel-mar 44
 
1267 daniel-mar 45
                if (!str_starts_with($maskcode,'V2:')) {
46
                        return false;
47
                } else {
48
                        $maskcode = substr($maskcode, 3);
49
                }
50
 
1268 daniel-mar 51
                if ($maskcode == '') return false;
52
 
1267 daniel-mar 53
                // Step 1: Split severities from the rest of the maskcodes
54
                /*
55
                 * "[ERR]AAA(BBB)+CCC(DDD)"   ==> array(
56
                 *                                 array(array("ERR"),"AAA(BBB)"),
57
                 *                                 array(array("ERR"),"CCC(DDD)")
58
                 *                              )
59
                 * "[INFO]AAA(B+BB)+[WARN]CCC(DDD)"  ==> array(
60
                 *                                 array(array("INFO"),"AAA(B+BB)"),
61
                 *                                 array(array("WARN"),"CCC(DDD)")
62
                 *                              )
63
                 * "[OK/WARN] AAA(B\)BB)+CCC(DDD)" ==> array(
64
                 *                                 array(array("OK", "WARN"),"AAA(B\)BB)"),
65
                 *                                 array(array("OK", "WARN"),"CCC(DDD)")
66
                 *                              )
67
                 */
188 daniel-mar 68
                $code = '';
288 daniel-mar 69
                $sev = '';
188 daniel-mar 70
                $bracket_level = 0;
288 daniel-mar 71
                $is_escaping = false;
72
                $inside_severity_block = false;
1267 daniel-mar 73
                for ($i=0; $i<strlen($maskcode); $i++) {
74
                        $char = $maskcode[$i];
289 daniel-mar 75
 
288 daniel-mar 76
                        if ($inside_severity_block) {
77
                                // Severity block (optional)
1267 daniel-mar 78
                                // e.g.  [OK/WARN] ==> $sevs = array("OK", "WARN")
288 daniel-mar 79
                                if ($char == '\\') {
80
                                        if ($is_escaping) {
81
                                                $is_escaping = false;
82
                                                $sev .= $char;
83
                                        } else {
84
                                                $is_escaping = true;
85
                                        }
86
                                }
87
                                else if ($char == '[') {
88
                                        if ($is_escaping) {
89
                                                $is_escaping = false;
90
                                        } else {
91
                                                $bracket_level++;
92
                                        }
93
                                        $sev .= $char;
94
                                }
95
                                else if ($char == ']') {
96
                                        if ($is_escaping) {
97
                                                $is_escaping = false;
98
                                                $sev .= $char;
99
                                        } else {
100
                                                $bracket_level--;
101
                                                if ($bracket_level < 0) return false;
102
                                                if ($bracket_level == 0) {
103
                                                        $inside_severity_block = false;
104
                                                        if ($sev != '') $sevs[] = $sev;
105
                                                        $sev = '';
106
                                                } else {
107
                                                        $sev .= $char;
108
                                                }
109
                                        }
110
                                }
111
                                else if ((($char == '/')) && ($bracket_level == 1)) {
112
                                        if ($is_escaping) {
113
                                                $is_escaping = false;
114
                                                $sev .= $char;
115
                                        } else {
116
                                                if ($sev != '') $sevs[] = $sev;
117
                                                $sev = '';
118
                                        }
119
                                } else {
120
                                        if ($is_escaping) {
121
                                                // This would actually be an error, because we cannot escape this
122
                                                $is_escaping = false;
123
                                                $sev .= '\\' . $char;
124
                                        } else {
125
                                                $sev .= $char;
126
                                        }
127
                                }
188 daniel-mar 128
                        } else {
288 daniel-mar 129
                                // Normal data (after the severity block)
130
                                if (($char == '[') && ($code == '')) {
131
                                        $inside_severity_block = true;
132
                                        $bracket_level++;
133
                                        $sevs = array();
134
                                }
135
                                else if ($char == '\\') {
136
                                        if ($is_escaping) {
137
                                                $is_escaping = false;
138
                                                $code .= $char;
139
                                        } else {
140
                                                $is_escaping = true;
141
                                        }
142
                                }
143
                                else if ($char == '(') {
144
                                        if ($is_escaping) {
145
                                                $is_escaping = false;
146
                                        } else {
147
                                                $bracket_level++;
148
                                        }
149
                                        $code .= $char;
150
                                }
151
                                else if ($char == ')') {
152
                                        if ($is_escaping) {
153
                                                $is_escaping = false;
154
                                        } else {
155
                                                $bracket_level--;
156
                                                if ($bracket_level < 0) return false;
157
                                        }
158
                                        $code .= $char;
159
                                }
1267 daniel-mar 160
                                else if (($char == '+') && ($bracket_level == 0)) {
288 daniel-mar 161
                                        if ($is_escaping) {
162
                                                $is_escaping = false;
163
                                                $code .= $char;
164
                                        } else {
165
                                                if ($code != '') $out[] = array($sevs,$code);
166
                                                $code = '';
167
                                        }
168
                                } else {
169
                                        if ($is_escaping) {
170
                                                // This would actually be an error, because we cannot escape this
171
                                                $is_escaping = false;
172
                                                $code .= '\\' . $char;
173
                                        } else {
174
                                                $code .= $char;
175
                                        }
176
                                }
188 daniel-mar 177
                        }
178
                }
288 daniel-mar 179
                if ($code != '') $out[] = array($sevs,$code);
180
                if ($inside_severity_block) return false;
1267 daniel-mar 181
                unset($sevs);
188 daniel-mar 182
 
1267 daniel-mar 183
                // Step 2: Process severities (split to online/offline)
184
                // Allowed:  ['INFO'] or ['INFO', 'INFO']
185
                // Disallow: ['NONE'] and ['NONE', 'NONE']
186
                foreach ($out as &$component) {
187
                        $sev_fixed = null;
188
                        $sevs = $component[0];
189
                        if (count($sevs) == 1) {
190
                                if ($sevs[0] == 'NONE') return false; // meaningless component
191
                                try { self::convertSeverity($sevs[0]); } catch (\Exception $e) { return false; } // just checking for valid value
192
                                $sev_fixed = $sevs[0];
193
                        } else if (count($sevs) == 2) {
194
                                $sev_online = $sevs[0];
1290 daniel-mar 195
                                $sev_offline = $sevs[1];
1267 daniel-mar 196
                                if (($sev_online == 'NONE') && ($sev_offline == 'NONE')) return false; // meaningless component
197
                                try { self::convertSeverity($sev_online); } catch (\Exception $e) { return false; } // just checking for valid value
198
                                try { self::convertSeverity($sev_offline); } catch (\Exception $e) { return false; } // just checking for valid value
199
                                $sev_fixed = [$sev_online, $sev_offline];
200
                        } else {
201
                                return false;
202
                        }
203
                        $component[0] = $sev_fixed;
204
                }
1316 daniel-mar 205
                unset($component);
1267 daniel-mar 206
 
207
                // Step 3: Process target (split to type and value)
208
                // 'OID(2.999)' becomes ['OID', '2.999']
209
                // 'A' becomes ['A']
210
                foreach ($out as &$component) {
211
                        $m = array();
212
                        if (preg_match('@^([^()]+)\((.+)\)$@ismU', $component[1], $m)) {
213
                                $type = $m[1];
214
                                $value = $m[2];
215
                                $component[1] = [$type, $value];
216
                        } else {
217
                                $component[1] = [$component[1]];
218
                        }
219
                }
1316 daniel-mar 220
                unset($component);
1267 daniel-mar 221
 
222
                // Some other checks (it makes it easier to validate the maskcodes with dev tools)
223
                foreach ($out as list($severity,$target)) {
224
                        if (($target[0] == 'OID') || ($target[0] == 'SUPOID')) {
225
                                if (is_array($severity)) return false; // OID and SUPOID logger mask cannot have online/offline severity
226
                                if (empty($target[1])) return false; /** @phpstan-ignore-line */
227
                        } else if (($target[0] == 'OIDRA') || ($target[0] == 'SUPOIDRA') || ($target[0] == 'RA')) {
228
                                if (empty($target[1])) return false;
229
                        } else if ($target[0] == 'A') {
230
                                if (!empty($target[1])) return false;
231
                        } else {
232
                                return false;
233
                        }
234
                }
235
 
188 daniel-mar 236
                return $out;
237
        }
238
 
1185 daniel-mar 239
        private $missing_plugin_queue = array();
825 daniel-mar 240
 
1116 daniel-mar 241
        /**
242
         * @return bool
243
         * @throws OIDplusException
244
         */
1185 daniel-mar 245
        public function reLogMissing(): bool {
246
                while (count($this->missing_plugin_queue) > 0) {
247
                        $item = $this->missing_plugin_queue[0];
248
                        if (!$this->log_internal($item[0], $item[1], false)) return false;
249
                        array_shift($this->missing_plugin_queue);
825 daniel-mar 250
                }
251
                return true;
252
        }
253
 
1116 daniel-mar 254
        /**
1267 daniel-mar 255
         * @param string $maskcode A description of the mask-codes can be found in doc/developer_notes/logger_maskcodes.md
1199 daniel-mar 256
         * @param string $message The message of the event
1267 daniel-mar 257
         * @param mixed ...$sprintfArgs If used, %1..%n in $maskcode and $message will be replaced, like _L() does.
1116 daniel-mar 258
         * @return bool
259
         * @throws OIDplusException
260
         */
1267 daniel-mar 261
        public function log(string $maskcode, string $message, ...$sprintfArgs): bool {
1185 daniel-mar 262
                $this->reLogMissing(); // try to re-log failed requests
1199 daniel-mar 263
 
1267 daniel-mar 264
                $sprintfArgs_Escaped = array();
265
                foreach ($sprintfArgs as $arg) {
266
                        // Inside an severity block, e.g. INFO of [INFO], we would need to escape []/\
267
                        // In the value, e.g. 2.999 of OID(2.999), we would need to escape ()+\
268
                        // Since there seems to be no meaningful use-case for parametrized severities, we only escape the value
269
                        $sprintfArgs_Escaped[] = str_replace(array('(',')','+','\\'), array('\\(', '\\)', '\\+', '\\\\'), $arg);
270
                }
271
 
272
                $maskcode = my_vsprintf($maskcode, $sprintfArgs_Escaped);
1199 daniel-mar 273
                $message = my_vsprintf($message, $sprintfArgs);
274
 
1267 daniel-mar 275
                if (strpos(str_replace('%%','',$maskcode),'%') !== false) {
1199 daniel-mar 276
                        throw new OIDplusException(_L('Unresolved wildcards in logging maskcode'));
277
                }
278
 
1267 daniel-mar 279
                return $this->log_internal($maskcode, $message, true);
825 daniel-mar 280
        }
281
 
1116 daniel-mar 282
        /**
1267 daniel-mar 283
         * @param string $sev_name
284
         * @return int
285
         * @throws OIDplusConfigInitializationException
286
         * @throws OIDplusException
287
         */
288
        private static function convertSeverity(string $sev_name): int {
289
                //$sev_name = strtoupper($sev_name);
290
 
291
                switch ($sev_name) {
292
                        case 'NONE':
293
                                // Do not log anything. Used for online/offline severity pairs
294
                                return -1;
295
 
296
                        // [OK]   = Success
297
                        //          Numeric value: 1
298
                        //          Rule of thumb: YOU have done something and it was successful
299
                        case  'OK':
300
                                return 1;
301
 
302
                        // [INFO] = Informational
303
                        //          Numeric value: 2
304
                        //          Rule of thumb: Someone else has done something (that affects you) and it was successful
305
                        case 'INFO':
306
                                return 2;
307
 
308
                        // [WARN] = Warning
309
                        //          Numeric value: 3
310
                        //          Rule of thumb: Something happened (probably someone did something) and it affects you
311
                        case 'WARN':
312
                                return 3;
313
 
314
                        // [ERR]  = Error
315
                        //          Numeric value: 4
316
                        //          Rule of thumb: Something failed (probably someone did something) and it affects you
317
                        case 'ERR':
318
                                return 4;
319
 
320
                        // [CRIT] = Critical
321
                        //          Numeric value: 5
322
                        //          Rule of thumb: Something happened (probably someone did something) which is not an error,
323
                        //          but some critical situation (e.g. hardware failure), and it affects you
324
                        case 'CRIT':
325
                                return 5;
326
 
327
                        default:
328
                                throw new OIDplusException(_L('Unknown severity "%1" in logger maskcode',$sev_name));
329
                }
330
        }
331
 
332
        /**
333
         * @param string $maskcode
1197 daniel-mar 334
         * @param string $message
1116 daniel-mar 335
         * @param bool $allow_delayed_log
336
         * @return bool
337
         * @throws OIDplusException
338
         */
1267 daniel-mar 339
        private function log_internal(string $maskcode, string $message, bool $allow_delayed_log): bool {
289 daniel-mar 340
                $loggerPlugins = OIDplus::getLoggerPlugins();
825 daniel-mar 341
                if (count($loggerPlugins) == 0) {
342
                        // The plugin might not be initialized in OIDplus::init()
343
                        // yet. Remember the log entries for later submission during
344
                        // OIDplus::init();
1267 daniel-mar 345
                        if ($allow_delayed_log) $this->missing_plugin_queue[] = array($maskcode, $message);
825 daniel-mar 346
                        return false;
347
                }
289 daniel-mar 348
 
1197 daniel-mar 349
                $logEvent = new OIDplusLogEvent($message);
115 daniel-mar 350
 
1267 daniel-mar 351
                $maskcode_ary = self::parse_maskcode($maskcode);
352
                if ($maskcode_ary === false) {
353
                        throw new OIDplusException(_L('Invalid maskcode "%1" (failed to parse or has invalid data)',$maskcode));
188 daniel-mar 354
                }
1267 daniel-mar 355
                foreach ($maskcode_ary as list($severity,$target)) {
356
                        if ($target[0] == 'OID') {
357
                                // OID(x)       Save log entry into the logbook of: Object "x"
358
                                $object_id = $target[1];
359
                                assert(!is_array($severity));
360
                                $obj = OIDplusObject::parse($object_id);
361
                                if (!$obj) throw new OIDplusException(_L('OID logger mask: Invalid object %1',$object_id));
362
                                if (($severity_int = self::convertSeverity($severity)) >= 0) {
363
                                        $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $object_id));
288 daniel-mar 364
                                }
365
                        }
289 daniel-mar 366
 
1267 daniel-mar 367
                        else if ($target[0] == 'SUPOID') {
368
                                // SUPOID(x)    Save log entry into the logbook of: Parent of object "x"
369
                                $object_id = $target[1];
370
                                assert(!is_array($severity));
288 daniel-mar 371
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 372
                                if (!$obj) throw new OIDplusException(_L('SUPOID logger mask: Invalid object %1',$object_id));
373
                                if ($objParent = $obj->getParent()) {
374
                                        $parent = $objParent->nodeId();
375
                                        if (($severity_int = self::convertSeverity($severity)) >= 0) {
376
                                                $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $parent));
419 daniel-mar 377
                                        }
288 daniel-mar 378
                                } else {
1267 daniel-mar 379
                                        //throw new OIDplusException(_L('%1 has no parent',$object_id));
288 daniel-mar 380
                                }
381
                        }
382
 
1267 daniel-mar 383
                        else if ($target[0] == 'OIDRA') {
384
                                // OIDRA(x)     Save log entry into the logbook of: Logged in RA of object "x"
385
                                $object_id = $target[1];
115 daniel-mar 386
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 387
                                if (!$obj) throw new OIDplusException(_L('OIDRA logger mask: Invalid object "%1"', $object_id));
388
                                if (!is_array($severity)) {
389
                                        $severity_online = $severity;
390
                                        $severity_offline = $severity;
391
                                } else {
392
                                        $severity_online = $severity[0];
393
                                        $severity_offline = $severity[1];
394
                                }
395
                                foreach (OIDplusRA::getAllRAs() as $ra) {
396
                                        if ($obj->userHasWriteRights($ra)) {
1350 daniel-mar 397
                                                try {
398
                                                        $tmp = OIDplus::authUtils()->isRaLoggedIn($ra);
399
                                                } catch (\Exception $e) {
400
                                                        $tmp = false; // avoid that logging fails if things like JWT signature verification fails
401
                                                }
402
                                                if ($tmp) {
1267 daniel-mar 403
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
404
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
405
                                                        }
406
                                                } else {
407
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
408
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
409
                                                        }
116 daniel-mar 410
                                                }
115 daniel-mar 411
                                        }
412
                                }
413
                        }
414
 
1267 daniel-mar 415
                        else if ($target[0] == 'SUPOIDRA') {
416
                                // SUPOIDRA(x)  Save log entry into the logbook of: Logged in RA that owns the superior object of "x"
417
                                $object_id = $target[1];
115 daniel-mar 418
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 419
                                if (!$obj) throw new OIDplusException(_L('SUPOIDRA logger mask: Invalid object "%1"',$object_id));
420
                                if (!is_array($severity)) {
421
                                        $severity_online = $severity;
422
                                        $severity_offline = $severity;
423
                                } else {
424
                                        $severity_online = $severity[0];
425
                                        $severity_offline = $severity[1];
426
                                }
427
                                foreach (OIDplusRA::getAllRAs() as $ra) {
428
                                        if ($obj->userHasParentalWriteRights($ra)) {
1350 daniel-mar 429
                                                try {
430
                                                        $tmp = OIDplus::authUtils()->isRaLoggedIn($ra);
431
                                                } catch (\Exception $e) {
432
                                                        $tmp = false; // avoid that logging fails if things like JWT signature verification fails
433
                                                }
434
                                                if ($tmp) {
1267 daniel-mar 435
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
436
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
419 daniel-mar 437
                                                        }
438
                                                } else {
1267 daniel-mar 439
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
440
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
441
                                                        }
116 daniel-mar 442
                                                }
115 daniel-mar 443
                                        }
444
                                }
445
                        }
446
 
1267 daniel-mar 447
                        else if ($target[0] == 'RA') {
448
                                // RA(x)        Save log entry into the logbook of: Logged in RA "x"
449
                                $ra_email = $target[1];
450
                                if (!is_array($severity)) {
451
                                        $severity_online = $severity;
452
                                        $severity_offline = $severity;
453
                                } else {
454
                                        $severity_online = $severity[0];
455
                                        $severity_offline = $severity[1];
456
                                }
1350 daniel-mar 457
                                try {
458
                                        $tmp = OIDplus::authUtils()->isRaLoggedIn($ra_email);
459
                                } catch (\Exception $e) {
460
                                        $tmp = false; // avoid that logging fails if things like JWT signature verification fails
461
                                }
462
                                if ($tmp) {
1267 daniel-mar 463
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
464
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra_email));
288 daniel-mar 465
                                        }
1267 daniel-mar 466
                                } else {
467
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
468
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra_email));
469
                                        }
115 daniel-mar 470
                                }
471
                        }
472
 
1267 daniel-mar 473
                        else if ($target[0] == 'A') {
474
                                // A    Save log entry into the logbook of: A logged in admin
475
                                if (!is_array($severity)) {
476
                                        $severity_online = $severity;
477
                                        $severity_offline = $severity;
478
                                } else {
479
                                        $severity_online = $severity[0];
480
                                        $severity_offline = $severity[1];
115 daniel-mar 481
                                }
1350 daniel-mar 482
                                try {
483
                                        $tmp = OIDplus::authUtils()->isAdminLoggedIn();
484
                                } catch (\Exception $e) {
485
                                        $tmp = false; // avoid that logging fails if things like JWT signature verification fails
486
                                }
487
                                if ($tmp) {
1267 daniel-mar 488
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
489
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, 'admin'));
490
                                        }
491
                                } else {
492
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
493
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, 'admin'));
494
                                        }
495
                                }
115 daniel-mar 496
                        }
497
 
498
                        // Unexpected
499
                        else {
1267 daniel-mar 500
                                throw new OIDplusException(_L('Unexpected logger component type "%1" in mask code "%2"',$target[0],$maskcode));
115 daniel-mar 501
                        }
116 daniel-mar 502
                }
115 daniel-mar 503
 
117 daniel-mar 504
                // Now write the log message
505
 
289 daniel-mar 506
                $result = false;
117 daniel-mar 507
 
289 daniel-mar 508
                foreach ($loggerPlugins as $plugin) {
509
                        $reason = '';
510
                        if ($plugin->available($reason)) {
1197 daniel-mar 511
                                $result |= $plugin->log($logEvent);
289 daniel-mar 512
                        }
117 daniel-mar 513
                }
514
 
289 daniel-mar 515
                return $result;
115 daniel-mar 516
        }
730 daniel-mar 517
}