Subversion Repositories oidplus

Rev

Rev 1207 | Rev 1268 | 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
 
51
                // Step 1: Split severities from the rest of the maskcodes
52
                /*
53
                 * "[ERR]AAA(BBB)+CCC(DDD)"   ==> array(
54
                 *                                 array(array("ERR"),"AAA(BBB)"),
55
                 *                                 array(array("ERR"),"CCC(DDD)")
56
                 *                              )
57
                 * "[INFO]AAA(B+BB)+[WARN]CCC(DDD)"  ==> array(
58
                 *                                 array(array("INFO"),"AAA(B+BB)"),
59
                 *                                 array(array("WARN"),"CCC(DDD)")
60
                 *                              )
61
                 * "[OK/WARN] AAA(B\)BB)+CCC(DDD)" ==> array(
62
                 *                                 array(array("OK", "WARN"),"AAA(B\)BB)"),
63
                 *                                 array(array("OK", "WARN"),"CCC(DDD)")
64
                 *                              )
65
                 */
188 daniel-mar 66
                $code = '';
288 daniel-mar 67
                $sev = '';
188 daniel-mar 68
                $bracket_level = 0;
288 daniel-mar 69
                $is_escaping = false;
70
                $inside_severity_block = false;
1267 daniel-mar 71
                for ($i=0; $i<strlen($maskcode); $i++) {
72
                        $char = $maskcode[$i];
289 daniel-mar 73
 
288 daniel-mar 74
                        if ($inside_severity_block) {
75
                                // Severity block (optional)
1267 daniel-mar 76
                                // e.g.  [OK/WARN] ==> $sevs = array("OK", "WARN")
288 daniel-mar 77
                                if ($char == '\\') {
78
                                        if ($is_escaping) {
79
                                                $is_escaping = false;
80
                                                $sev .= $char;
81
                                        } else {
82
                                                $is_escaping = true;
83
                                        }
84
                                }
85
                                else if ($char == '[') {
86
                                        if ($is_escaping) {
87
                                                $is_escaping = false;
88
                                        } else {
89
                                                $bracket_level++;
90
                                        }
91
                                        $sev .= $char;
92
                                }
93
                                else if ($char == ']') {
94
                                        if ($is_escaping) {
95
                                                $is_escaping = false;
96
                                                $sev .= $char;
97
                                        } else {
98
                                                $bracket_level--;
99
                                                if ($bracket_level < 0) return false;
100
                                                if ($bracket_level == 0) {
101
                                                        $inside_severity_block = false;
102
                                                        if ($sev != '') $sevs[] = $sev;
103
                                                        $sev = '';
104
                                                } else {
105
                                                        $sev .= $char;
106
                                                }
107
                                        }
108
                                }
109
                                else if ((($char == '/')) && ($bracket_level == 1)) {
110
                                        if ($is_escaping) {
111
                                                $is_escaping = false;
112
                                                $sev .= $char;
113
                                        } else {
114
                                                if ($sev != '') $sevs[] = $sev;
115
                                                $sev = '';
116
                                        }
117
                                } else {
118
                                        if ($is_escaping) {
119
                                                // This would actually be an error, because we cannot escape this
120
                                                $is_escaping = false;
121
                                                $sev .= '\\' . $char;
122
                                        } else {
123
                                                $sev .= $char;
124
                                        }
125
                                }
188 daniel-mar 126
                        } else {
288 daniel-mar 127
                                // Normal data (after the severity block)
128
                                if (($char == '[') && ($code == '')) {
129
                                        $inside_severity_block = true;
130
                                        $bracket_level++;
131
                                        $sevs = array();
132
                                }
133
                                else if ($char == '\\') {
134
                                        if ($is_escaping) {
135
                                                $is_escaping = false;
136
                                                $code .= $char;
137
                                        } else {
138
                                                $is_escaping = true;
139
                                        }
140
                                }
141
                                else if ($char == '(') {
142
                                        if ($is_escaping) {
143
                                                $is_escaping = false;
144
                                        } else {
145
                                                $bracket_level++;
146
                                        }
147
                                        $code .= $char;
148
                                }
149
                                else if ($char == ')') {
150
                                        if ($is_escaping) {
151
                                                $is_escaping = false;
152
                                        } else {
153
                                                $bracket_level--;
154
                                                if ($bracket_level < 0) return false;
155
                                        }
156
                                        $code .= $char;
157
                                }
1267 daniel-mar 158
                                else if (($char == '+') && ($bracket_level == 0)) {
288 daniel-mar 159
                                        if ($is_escaping) {
160
                                                $is_escaping = false;
161
                                                $code .= $char;
162
                                        } else {
163
                                                if ($code != '') $out[] = array($sevs,$code);
164
                                                $code = '';
165
                                        }
166
                                } else {
167
                                        if ($is_escaping) {
168
                                                // This would actually be an error, because we cannot escape this
169
                                                $is_escaping = false;
170
                                                $code .= '\\' . $char;
171
                                        } else {
172
                                                $code .= $char;
173
                                        }
174
                                }
188 daniel-mar 175
                        }
176
                }
288 daniel-mar 177
                if ($code != '') $out[] = array($sevs,$code);
178
                if ($inside_severity_block) return false;
1267 daniel-mar 179
                unset($sevs);
188 daniel-mar 180
 
1267 daniel-mar 181
                // Step 2: Process severities (split to online/offline)
182
                // Allowed:  ['INFO'] or ['INFO', 'INFO']
183
                // Disallow: ['NONE'] and ['NONE', 'NONE']
184
                foreach ($out as &$component) {
185
                        $sev_fixed = null;
186
                        $sevs = $component[0];
187
                        if (count($sevs) == 1) {
188
                                if ($sevs[0] == 'NONE') return false; // meaningless component
189
                                try { self::convertSeverity($sevs[0]); } catch (\Exception $e) { return false; } // just checking for valid value
190
                                $sev_fixed = $sevs[0];
191
                        } else if (count($sevs) == 2) {
192
                                $sev_online = $sevs[0];
193
                                $sev_offline = $sevs[1];
194
                                if (($sev_online == 'NONE') && ($sev_offline == 'NONE')) return false; // meaningless component
195
                                try { self::convertSeverity($sev_online); } catch (\Exception $e) { return false; } // just checking for valid value
196
                                try { self::convertSeverity($sev_offline); } catch (\Exception $e) { return false; } // just checking for valid value
197
                                $sev_fixed = [$sev_online, $sev_offline];
198
                        } else {
199
                                return false;
200
                        }
201
                        $component[0] = $sev_fixed;
202
                }
203
 
204
                // Step 3: Process target (split to type and value)
205
                // 'OID(2.999)' becomes ['OID', '2.999']
206
                // 'A' becomes ['A']
207
                foreach ($out as &$component) {
208
                        $m = array();
209
                        if (preg_match('@^([^()]+)\((.+)\)$@ismU', $component[1], $m)) {
210
                                $type = $m[1];
211
                                $value = $m[2];
212
                                $component[1] = [$type, $value];
213
                        } else {
214
                                $component[1] = [$component[1]];
215
                        }
216
                }
217
 
218
                // Some other checks (it makes it easier to validate the maskcodes with dev tools)
219
                foreach ($out as list($severity,$target)) {
220
                        if (($target[0] == 'OID') || ($target[0] == 'SUPOID')) {
221
                                if (is_array($severity)) return false; // OID and SUPOID logger mask cannot have online/offline severity
222
                                if (empty($target[1])) return false; /** @phpstan-ignore-line */
223
                        } else if (($target[0] == 'OIDRA') || ($target[0] == 'SUPOIDRA') || ($target[0] == 'RA')) {
224
                                if (empty($target[1])) return false;
225
                        } else if ($target[0] == 'A') {
226
                                if (!empty($target[1])) return false;
227
                        } else {
228
                                return false;
229
                        }
230
                }
231
 
188 daniel-mar 232
                return $out;
233
        }
234
 
1185 daniel-mar 235
        private $missing_plugin_queue = array();
825 daniel-mar 236
 
1116 daniel-mar 237
        /**
238
         * @return bool
239
         * @throws OIDplusException
240
         */
1185 daniel-mar 241
        public function reLogMissing(): bool {
242
                while (count($this->missing_plugin_queue) > 0) {
243
                        $item = $this->missing_plugin_queue[0];
244
                        if (!$this->log_internal($item[0], $item[1], false)) return false;
245
                        array_shift($this->missing_plugin_queue);
825 daniel-mar 246
                }
247
                return true;
248
        }
249
 
1116 daniel-mar 250
        /**
1267 daniel-mar 251
         * @param string $maskcode A description of the mask-codes can be found in doc/developer_notes/logger_maskcodes.md
1199 daniel-mar 252
         * @param string $message The message of the event
1267 daniel-mar 253
         * @param mixed ...$sprintfArgs If used, %1..%n in $maskcode and $message will be replaced, like _L() does.
1116 daniel-mar 254
         * @return bool
255
         * @throws OIDplusException
256
         */
1267 daniel-mar 257
        public function log(string $maskcode, string $message, ...$sprintfArgs): bool {
1185 daniel-mar 258
                $this->reLogMissing(); // try to re-log failed requests
1199 daniel-mar 259
 
1267 daniel-mar 260
                $sprintfArgs_Escaped = array();
261
                foreach ($sprintfArgs as $arg) {
262
                        // Inside an severity block, e.g. INFO of [INFO], we would need to escape []/\
263
                        // In the value, e.g. 2.999 of OID(2.999), we would need to escape ()+\
264
                        // Since there seems to be no meaningful use-case for parametrized severities, we only escape the value
265
                        $sprintfArgs_Escaped[] = str_replace(array('(',')','+','\\'), array('\\(', '\\)', '\\+', '\\\\'), $arg);
266
                }
267
 
268
                $maskcode = my_vsprintf($maskcode, $sprintfArgs_Escaped);
1199 daniel-mar 269
                $message = my_vsprintf($message, $sprintfArgs);
270
 
1267 daniel-mar 271
                if (strpos(str_replace('%%','',$maskcode),'%') !== false) {
1199 daniel-mar 272
                        throw new OIDplusException(_L('Unresolved wildcards in logging maskcode'));
273
                }
274
 
1267 daniel-mar 275
                return $this->log_internal($maskcode, $message, true);
825 daniel-mar 276
        }
277
 
1116 daniel-mar 278
        /**
1267 daniel-mar 279
         * @param string $sev_name
280
         * @return int
281
         * @throws OIDplusConfigInitializationException
282
         * @throws OIDplusException
283
         */
284
        private static function convertSeverity(string $sev_name): int {
285
                //$sev_name = strtoupper($sev_name);
286
 
287
                switch ($sev_name) {
288
                        case 'NONE':
289
                                // Do not log anything. Used for online/offline severity pairs
290
                                return -1;
291
 
292
                        // [OK]   = Success
293
                        //          Numeric value: 1
294
                        //          Rule of thumb: YOU have done something and it was successful
295
                        case  'OK':
296
                                return 1;
297
 
298
                        // [INFO] = Informational
299
                        //          Numeric value: 2
300
                        //          Rule of thumb: Someone else has done something (that affects you) and it was successful
301
                        case 'INFO':
302
                                return 2;
303
 
304
                        // [WARN] = Warning
305
                        //          Numeric value: 3
306
                        //          Rule of thumb: Something happened (probably someone did something) and it affects you
307
                        case 'WARN':
308
                                return 3;
309
 
310
                        // [ERR]  = Error
311
                        //          Numeric value: 4
312
                        //          Rule of thumb: Something failed (probably someone did something) and it affects you
313
                        case 'ERR':
314
                                return 4;
315
 
316
                        // [CRIT] = Critical
317
                        //          Numeric value: 5
318
                        //          Rule of thumb: Something happened (probably someone did something) which is not an error,
319
                        //          but some critical situation (e.g. hardware failure), and it affects you
320
                        case 'CRIT':
321
                                return 5;
322
 
323
                        default:
324
                                throw new OIDplusException(_L('Unknown severity "%1" in logger maskcode',$sev_name));
325
                }
326
        }
327
 
328
        /**
329
         * @param string $maskcode
1197 daniel-mar 330
         * @param string $message
1116 daniel-mar 331
         * @param bool $allow_delayed_log
332
         * @return bool
333
         * @throws OIDplusException
334
         */
1267 daniel-mar 335
        private function log_internal(string $maskcode, string $message, bool $allow_delayed_log): bool {
289 daniel-mar 336
                $loggerPlugins = OIDplus::getLoggerPlugins();
825 daniel-mar 337
                if (count($loggerPlugins) == 0) {
338
                        // The plugin might not be initialized in OIDplus::init()
339
                        // yet. Remember the log entries for later submission during
340
                        // OIDplus::init();
1267 daniel-mar 341
                        if ($allow_delayed_log) $this->missing_plugin_queue[] = array($maskcode, $message);
825 daniel-mar 342
                        return false;
343
                }
289 daniel-mar 344
 
1197 daniel-mar 345
                $logEvent = new OIDplusLogEvent($message);
115 daniel-mar 346
 
1267 daniel-mar 347
                $maskcode_ary = self::parse_maskcode($maskcode);
348
                if ($maskcode_ary === false) {
349
                        throw new OIDplusException(_L('Invalid maskcode "%1" (failed to parse or has invalid data)',$maskcode));
188 daniel-mar 350
                }
1267 daniel-mar 351
                foreach ($maskcode_ary as list($severity,$target)) {
352
                        if ($target[0] == 'OID') {
353
                                // OID(x)       Save log entry into the logbook of: Object "x"
354
                                $object_id = $target[1];
355
                                assert(!is_array($severity));
356
                                $obj = OIDplusObject::parse($object_id);
357
                                if (!$obj) throw new OIDplusException(_L('OID logger mask: Invalid object %1',$object_id));
358
                                if (($severity_int = self::convertSeverity($severity)) >= 0) {
359
                                        $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $object_id));
288 daniel-mar 360
                                }
361
                        }
289 daniel-mar 362
 
1267 daniel-mar 363
                        else if ($target[0] == 'SUPOID') {
364
                                // SUPOID(x)    Save log entry into the logbook of: Parent of object "x"
365
                                $object_id = $target[1];
366
                                assert(!is_array($severity));
288 daniel-mar 367
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 368
                                if (!$obj) throw new OIDplusException(_L('SUPOID logger mask: Invalid object %1',$object_id));
369
                                if ($objParent = $obj->getParent()) {
370
                                        $parent = $objParent->nodeId();
371
                                        if (($severity_int = self::convertSeverity($severity)) >= 0) {
372
                                                $logEvent->addTarget(new OIDplusLogTargetObject($severity_int, $parent));
419 daniel-mar 373
                                        }
288 daniel-mar 374
                                } else {
1267 daniel-mar 375
                                        //throw new OIDplusException(_L('%1 has no parent',$object_id));
288 daniel-mar 376
                                }
377
                        }
378
 
1267 daniel-mar 379
                        else if ($target[0] == 'OIDRA') {
380
                                // OIDRA(x)     Save log entry into the logbook of: Logged in RA of object "x"
381
                                $object_id = $target[1];
115 daniel-mar 382
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 383
                                if (!$obj) throw new OIDplusException(_L('OIDRA logger mask: Invalid object "%1"', $object_id));
384
                                if (!is_array($severity)) {
385
                                        $severity_online = $severity;
386
                                        $severity_offline = $severity;
387
                                } else {
388
                                        $severity_online = $severity[0];
389
                                        $severity_offline = $severity[1];
390
                                }
391
                                foreach (OIDplusRA::getAllRAs() as $ra) {
392
                                        if ($obj->userHasWriteRights($ra)) {
393
                                                if (OIDplus::authUtils()->isRaLoggedIn($ra)) {
394
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
395
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
396
                                                        }
397
                                                } else {
398
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
399
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
400
                                                        }
116 daniel-mar 401
                                                }
115 daniel-mar 402
                                        }
403
                                }
404
                        }
405
 
1267 daniel-mar 406
                        else if ($target[0] == 'SUPOIDRA') {
407
                                // SUPOIDRA(x)  Save log entry into the logbook of: Logged in RA that owns the superior object of "x"
408
                                $object_id = $target[1];
115 daniel-mar 409
                                $obj = OIDplusObject::parse($object_id);
1267 daniel-mar 410
                                if (!$obj) throw new OIDplusException(_L('SUPOIDRA logger mask: Invalid object "%1"',$object_id));
411
                                if (!is_array($severity)) {
412
                                        $severity_online = $severity;
413
                                        $severity_offline = $severity;
414
                                } else {
415
                                        $severity_online = $severity[0];
416
                                        $severity_offline = $severity[1];
417
                                }
418
                                foreach (OIDplusRA::getAllRAs() as $ra) {
419
                                        if ($obj->userHasParentalWriteRights($ra)) {
420
                                                if (OIDplus::authUtils()->isRaLoggedIn($ra)) {
421
                                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
422
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
419 daniel-mar 423
                                                        }
424
                                                } else {
1267 daniel-mar 425
                                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
426
                                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
427
                                                        }
116 daniel-mar 428
                                                }
115 daniel-mar 429
                                        }
430
                                }
431
                        }
432
 
1267 daniel-mar 433
                        else if ($target[0] == 'RA') {
434
                                // RA(x)        Save log entry into the logbook of: Logged in RA "x"
435
                                $ra_email = $target[1];
436
                                if (!is_array($severity)) {
437
                                        $severity_online = $severity;
438
                                        $severity_offline = $severity;
439
                                } else {
440
                                        $severity_online = $severity[0];
441
                                        $severity_offline = $severity[1];
442
                                }
443
                                if (OIDplus::authUtils()->isRaLoggedIn($ra_email)) {
444
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
445
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra_email));
288 daniel-mar 446
                                        }
1267 daniel-mar 447
                                } else {
448
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
449
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra_email));
450
                                        }
115 daniel-mar 451
                                }
452
                        }
453
 
1267 daniel-mar 454
                        else if ($target[0] == 'A') {
455
                                // A    Save log entry into the logbook of: A logged in admin
456
                                if (!is_array($severity)) {
457
                                        $severity_online = $severity;
458
                                        $severity_offline = $severity;
459
                                } else {
460
                                        $severity_online = $severity[0];
461
                                        $severity_offline = $severity[1];
115 daniel-mar 462
                                }
1267 daniel-mar 463
                                if (OIDplus::authUtils()->isAdminLoggedIn()) {
464
                                        if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
465
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, 'admin'));
466
                                        }
467
                                } else {
468
                                        if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
469
                                                $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, 'admin'));
470
                                        }
471
                                }
115 daniel-mar 472
                        }
473
 
474
                        // Unexpected
475
                        else {
1267 daniel-mar 476
                                throw new OIDplusException(_L('Unexpected logger component type "%1" in mask code "%2"',$target[0],$maskcode));
115 daniel-mar 477
                        }
116 daniel-mar 478
                }
115 daniel-mar 479
 
117 daniel-mar 480
                // Now write the log message
481
 
289 daniel-mar 482
                $result = false;
117 daniel-mar 483
 
289 daniel-mar 484
                foreach ($loggerPlugins as $plugin) {
485
                        $reason = '';
486
                        if ($plugin->available($reason)) {
1197 daniel-mar 487
                                $result |= $plugin->log($logEvent);
289 daniel-mar 488
                        }
117 daniel-mar 489
                }
490
 
289 daniel-mar 491
                return $result;
115 daniel-mar 492
        }
730 daniel-mar 493
}