Subversion Repositories oidplus

Rev

Rev 1268 | Rev 1290 | 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];
1285 daniel-mar 195
                                $sev_offline = $sevs[1]; /* @phpstan-ignore-line */
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
                }
205
 
206
                // Step 3: Process target (split to type and value)
207
                // 'OID(2.999)' becomes ['OID', '2.999']
208
                // 'A' becomes ['A']
209
                foreach ($out as &$component) {
210
                        $m = array();
211
                        if (preg_match('@^([^()]+)\((.+)\)$@ismU', $component[1], $m)) {
212
                                $type = $m[1];
213
                                $value = $m[2];
214
                                $component[1] = [$type, $value];
215
                        } else {
216
                                $component[1] = [$component[1]];
217
                        }
218
                }
219
 
220
                // Some other checks (it makes it easier to validate the maskcodes with dev tools)
221
                foreach ($out as list($severity,$target)) {
222
                        if (($target[0] == 'OID') || ($target[0] == 'SUPOID')) {
223
                                if (is_array($severity)) return false; // OID and SUPOID logger mask cannot have online/offline severity
224
                                if (empty($target[1])) return false; /** @phpstan-ignore-line */
225
                        } else if (($target[0] == 'OIDRA') || ($target[0] == 'SUPOIDRA') || ($target[0] == 'RA')) {
226
                                if (empty($target[1])) return false;
227
                        } else if ($target[0] == 'A') {
228
                                if (!empty($target[1])) return false;
229
                        } else {
230
                                return false;
231
                        }
232
                }
233
 
188 daniel-mar 234
                return $out;
235
        }
236
 
1185 daniel-mar 237
        private $missing_plugin_queue = array();
825 daniel-mar 238
 
1116 daniel-mar 239
        /**
240
         * @return bool
241
         * @throws OIDplusException
242
         */
1185 daniel-mar 243
        public function reLogMissing(): bool {
244
                while (count($this->missing_plugin_queue) > 0) {
245
                        $item = $this->missing_plugin_queue[0];
246
                        if (!$this->log_internal($item[0], $item[1], false)) return false;
247
                        array_shift($this->missing_plugin_queue);
825 daniel-mar 248
                }
249
                return true;
250
        }
251
 
1116 daniel-mar 252
        /**
1267 daniel-mar 253
         * @param string $maskcode A description of the mask-codes can be found in doc/developer_notes/logger_maskcodes.md
1199 daniel-mar 254
         * @param string $message The message of the event
1267 daniel-mar 255
         * @param mixed ...$sprintfArgs If used, %1..%n in $maskcode and $message will be replaced, like _L() does.
1116 daniel-mar 256
         * @return bool
257
         * @throws OIDplusException
258
         */
1267 daniel-mar 259
        public function log(string $maskcode, string $message, ...$sprintfArgs): bool {
1185 daniel-mar 260
                $this->reLogMissing(); // try to re-log failed requests
1199 daniel-mar 261
 
1267 daniel-mar 262
                $sprintfArgs_Escaped = array();
263
                foreach ($sprintfArgs as $arg) {
264
                        // Inside an severity block, e.g. INFO of [INFO], we would need to escape []/\
265
                        // In the value, e.g. 2.999 of OID(2.999), we would need to escape ()+\
266
                        // Since there seems to be no meaningful use-case for parametrized severities, we only escape the value
267
                        $sprintfArgs_Escaped[] = str_replace(array('(',')','+','\\'), array('\\(', '\\)', '\\+', '\\\\'), $arg);
268
                }
269
 
270
                $maskcode = my_vsprintf($maskcode, $sprintfArgs_Escaped);
1199 daniel-mar 271
                $message = my_vsprintf($message, $sprintfArgs);
272
 
1267 daniel-mar 273
                if (strpos(str_replace('%%','',$maskcode),'%') !== false) {
1199 daniel-mar 274
                        throw new OIDplusException(_L('Unresolved wildcards in logging maskcode'));
275
                }
276
 
1267 daniel-mar 277
                return $this->log_internal($maskcode, $message, true);
825 daniel-mar 278
        }
279
 
1116 daniel-mar 280
        /**
1267 daniel-mar 281
         * @param string $sev_name
282
         * @return int
283
         * @throws OIDplusConfigInitializationException
284
         * @throws OIDplusException
285
         */
286
        private static function convertSeverity(string $sev_name): int {
287
                //$sev_name = strtoupper($sev_name);
288
 
289
                switch ($sev_name) {
290
                        case 'NONE':
291
                                // Do not log anything. Used for online/offline severity pairs
292
                                return -1;
293
 
294
                        // [OK]   = Success
295
                        //          Numeric value: 1
296
                        //          Rule of thumb: YOU have done something and it was successful
297
                        case  'OK':
298
                                return 1;
299
 
300
                        // [INFO] = Informational
301
                        //          Numeric value: 2
302
                        //          Rule of thumb: Someone else has done something (that affects you) and it was successful
303
                        case 'INFO':
304
                                return 2;
305
 
306
                        // [WARN] = Warning
307
                        //          Numeric value: 3
308
                        //          Rule of thumb: Something happened (probably someone did something) and it affects you
309
                        case 'WARN':
310
                                return 3;
311
 
312
                        // [ERR]  = Error
313
                        //          Numeric value: 4
314
                        //          Rule of thumb: Something failed (probably someone did something) and it affects you
315
                        case 'ERR':
316
                                return 4;
317
 
318
                        // [CRIT] = Critical
319
                        //          Numeric value: 5
320
                        //          Rule of thumb: Something happened (probably someone did something) which is not an error,
321
                        //          but some critical situation (e.g. hardware failure), and it affects you
322
                        case 'CRIT':
323
                                return 5;
324
 
325
                        default:
326
                                throw new OIDplusException(_L('Unknown severity "%1" in logger maskcode',$sev_name));
327
                }
328
        }
329
 
330
        /**
331
         * @param string $maskcode
1197 daniel-mar 332
         * @param string $message
1116 daniel-mar 333
         * @param bool $allow_delayed_log
334
         * @return bool
335
         * @throws OIDplusException
336
         */
1267 daniel-mar 337
        private function log_internal(string $maskcode, string $message, bool $allow_delayed_log): bool {
289 daniel-mar 338
                $loggerPlugins = OIDplus::getLoggerPlugins();
825 daniel-mar 339
                if (count($loggerPlugins) == 0) {
340
                        // The plugin might not be initialized in OIDplus::init()
341
                        // yet. Remember the log entries for later submission during
342
                        // OIDplus::init();
1267 daniel-mar 343
                        if ($allow_delayed_log) $this->missing_plugin_queue[] = array($maskcode, $message);
825 daniel-mar 344
                        return false;
345
                }
289 daniel-mar 346
 
1197 daniel-mar 347
                $logEvent = new OIDplusLogEvent($message);
115 daniel-mar 348
 
1267 daniel-mar 349
                $maskcode_ary = self::parse_maskcode($maskcode);
350
                if ($maskcode_ary === false) {
351
                        throw new OIDplusException(_L('Invalid maskcode "%1" (failed to parse or has invalid data)',$maskcode));
188 daniel-mar 352
                }
1267 daniel-mar 353
                foreach ($maskcode_ary as list($severity,$target)) {
354
                        if ($target[0] == 'OID') {
355
                                // OID(x)       Save log entry into the logbook of: Object "x"
356
                                $object_id = $target[1];
357
                                assert(!is_array($severity));
358
                                $obj = OIDplusObject::parse($object_id);
359
                                if (!$obj) throw new OIDplusException(_L('OID logger mask: Invalid object %1',$object_id));
360
                                if (($severity_int = self::convertSeverity($severity)) >= 0) {
361
                                        $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $object_id));
288 daniel-mar 362
                                }
363
                        }
289 daniel-mar 364
 
1267 daniel-mar 365
                        else if ($target[0] == 'SUPOID') {
366
                                // SUPOID(x)    Save log entry into the logbook of: Parent of object "x"
367
                                $object_id = $target[1];
368
                                assert(!is_array($severity));
288 daniel-mar 369
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 370
                                if (!$obj) throw new OIDplusException(_L('SUPOID logger mask: Invalid object %1',$object_id));
371
                                if ($objParent = $obj->getParent()) {
372
                                        $parent = $objParent->nodeId();
373
                                        if (($severity_int = self::convertSeverity($severity)) >= 0) {
374
                                                $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $parent));
419 daniel-mar 375
                                        }
288 daniel-mar 376
                                } else {
1267 daniel-mar 377
                                        //throw new OIDplusException(_L('%1 has no parent',$object_id));
288 daniel-mar 378
                                }
379
                        }
380
 
1267 daniel-mar 381
                        else if ($target[0] == 'OIDRA') {
382
                                // OIDRA(x)     Save log entry into the logbook of: Logged in RA of object "x"
383
                                $object_id = $target[1];
115 daniel-mar 384
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 385
                                if (!$obj) throw new OIDplusException(_L('OIDRA logger mask: Invalid object "%1"', $object_id));
386
                                if (!is_array($severity)) {
387
                                        $severity_online = $severity;
388
                                        $severity_offline = $severity;
389
                                } else {
390
                                        $severity_online = $severity[0];
391
                                        $severity_offline = $severity[1];
392
                                }
393
                                foreach (OIDplusRA::getAllRAs() as $ra) {
394
                                        if ($obj->userHasWriteRights($ra)) {
395
                                                if (OIDplus::authUtils()->isRaLoggedIn($ra)) {
396
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
397
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
398
                                                        }
399
                                                } else {
400
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
401
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
402
                                                        }
116 daniel-mar 403
                                                }
115 daniel-mar 404
                                        }
405
                                }
406
                        }
407
 
1267 daniel-mar 408
                        else if ($target[0] == 'SUPOIDRA') {
409
                                // SUPOIDRA(x)  Save log entry into the logbook of: Logged in RA that owns the superior object of "x"
410
                                $object_id = $target[1];
115 daniel-mar 411
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 412
                                if (!$obj) throw new OIDplusException(_L('SUPOIDRA logger mask: Invalid object "%1"',$object_id));
413
                                if (!is_array($severity)) {
414
                                        $severity_online = $severity;
415
                                        $severity_offline = $severity;
416
                                } else {
417
                                        $severity_online = $severity[0];
418
                                        $severity_offline = $severity[1];
419
                                }
420
                                foreach (OIDplusRA::getAllRAs() as $ra) {
421
                                        if ($obj->userHasParentalWriteRights($ra)) {
422
                                                if (OIDplus::authUtils()->isRaLoggedIn($ra)) {
423
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
424
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
419 daniel-mar 425
                                                        }
426
                                                } else {
1267 daniel-mar 427
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
428
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
429
                                                        }
116 daniel-mar 430
                                                }
115 daniel-mar 431
                                        }
432
                                }
433
                        }
434
 
1267 daniel-mar 435
                        else if ($target[0] == 'RA') {
436
                                // RA(x)        Save log entry into the logbook of: Logged in RA "x"
437
                                $ra_email = $target[1];
438
                                if (!is_array($severity)) {
439
                                        $severity_online = $severity;
440
                                        $severity_offline = $severity;
441
                                } else {
442
                                        $severity_online = $severity[0];
443
                                        $severity_offline = $severity[1];
444
                                }
445
                                if (OIDplus::authUtils()->isRaLoggedIn($ra_email)) {
446
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
447
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra_email));
288 daniel-mar 448
                                        }
1267 daniel-mar 449
                                } else {
450
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
451
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra_email));
452
                                        }
115 daniel-mar 453
                                }
454
                        }
455
 
1267 daniel-mar 456
                        else if ($target[0] == 'A') {
457
                                // A    Save log entry into the logbook of: A logged in admin
458
                                if (!is_array($severity)) {
459
                                        $severity_online = $severity;
460
                                        $severity_offline = $severity;
461
                                } else {
462
                                        $severity_online = $severity[0];
463
                                        $severity_offline = $severity[1];
115 daniel-mar 464
                                }
1267 daniel-mar 465
                                if (OIDplus::authUtils()->isAdminLoggedIn()) {
466
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
467
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, 'admin'));
468
                                        }
469
                                } else {
470
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
471
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, 'admin'));
472
                                        }
473
                                }
115 daniel-mar 474
                        }
475
 
476
                        // Unexpected
477
                        else {
1267 daniel-mar 478
                                throw new OIDplusException(_L('Unexpected logger component type "%1" in mask code "%2"',$target[0],$maskcode));
115 daniel-mar 479
                        }
116 daniel-mar 480
                }
115 daniel-mar 481
 
117 daniel-mar 482
                // Now write the log message
483
 
289 daniel-mar 484
                $result = false;
117 daniel-mar 485
 
289 daniel-mar 486
                foreach ($loggerPlugins as $plugin) {
487
                        $reason = '';
488
                        if ($plugin->available($reason)) {
1197 daniel-mar 489
                                $result |= $plugin->log($logEvent);
289 daniel-mar 490
                        }
117 daniel-mar 491
                }
492
 
289 daniel-mar 493
                return $result;
115 daniel-mar 494
        }
730 daniel-mar 495
}