Subversion Repositories oidplus

Rev

Rev 1350 | Blame | Compare with Previous | Last modification | View Log | RSS feed

  1. <?php
  2.  
  3. /*
  4.  * OIDplus 2.0
  5.  * Copyright 2019 - 2023 Daniel Marschall, ViaThinkSoft
  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.  
  20. namespace ViaThinkSoft\OIDplus;
  21.  
  22. // phpcs:disable PSR1.Files.SideEffects
  23. \defined('INSIDE_OIDPLUS') or die;
  24. // phpcs:enable PSR1.Files.SideEffects
  25.  
  26. class OIDplusLogger extends OIDplusBaseClass {
  27.  
  28.         /**
  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
  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.
  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.
  40.          */
  41.         public static function parse_maskcode(string $maskcode) {
  42.                 $out = array();
  43.                 $sevs = array(); // Note: The severity block will repeat for the next components if not changed explicitly
  44.  
  45.                 if (!str_starts_with($maskcode,'V2:')) {
  46.                         return false;
  47.                 } else {
  48.                         $maskcode = substr($maskcode, 3);
  49.                 }
  50.  
  51.                 if ($maskcode == '') return false;
  52.  
  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.                  */
  68.                 $code = '';
  69.                 $sev = '';
  70.                 $bracket_level = 0;
  71.                 $is_escaping = false;
  72.                 $inside_severity_block = false;
  73.                 for ($i=0; $i<strlen($maskcode); $i++) {
  74.                         $char = $maskcode[$i];
  75.  
  76.                         if ($inside_severity_block) {
  77.                                 // Severity block (optional)
  78.                                 // e.g.  [OK/WARN] ==> $sevs = array("OK", "WARN")
  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.                                 }
  128.                         } else {
  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.                                 }
  160.                                 else if (($char == '+') && ($bracket_level == 0)) {
  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.                                 }
  177.                         }
  178.                 }
  179.                 if ($code != '') $out[] = array($sevs,$code);
  180.                 if ($inside_severity_block) return false;
  181.                 unset($sevs);
  182.  
  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];
  195.                                 $sev_offline = $sevs[1];
  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.                 unset($component);
  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.                 }
  220.                 unset($component);
  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.  
  236.                 return $out;
  237.         }
  238.  
  239.         private $missing_plugin_queue = array();
  240.  
  241.         /**
  242.          * @return bool
  243.          * @throws OIDplusException
  244.          */
  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);
  250.                 }
  251.                 return true;
  252.         }
  253.  
  254.         /**
  255.          * @param string $maskcode A description of the mask-codes can be found in doc/developer_notes/logger_maskcodes.md
  256.          * @param string $message The message of the event
  257.          * @param mixed ...$sprintfArgs If used, %1..%n in $maskcode and $message will be replaced, like _L() does.
  258.          * @return bool
  259.          * @throws OIDplusException
  260.          */
  261.         public function log(string $maskcode, string $message, ...$sprintfArgs): bool {
  262.                 $this->reLogMissing(); // try to re-log failed requests
  263.  
  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);
  273.                 $message = my_vsprintf($message, $sprintfArgs);
  274.  
  275.                 if (strpos(str_replace('%%','',$maskcode),'%') !== false) {
  276.                         throw new OIDplusException(_L('Unresolved wildcards in logging maskcode'));
  277.                 }
  278.  
  279.                 return $this->log_internal($maskcode, $message, true);
  280.         }
  281.  
  282.         /**
  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
  334.          * @param string $message
  335.          * @param bool $allow_delayed_log
  336.          * @return bool
  337.          * @throws OIDplusException
  338.          */
  339.         private function log_internal(string $maskcode, string $message, bool $allow_delayed_log): bool {
  340.                 $loggerPlugins = OIDplus::getLoggerPlugins();
  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();
  345.                         if ($allow_delayed_log) $this->missing_plugin_queue[] = array($maskcode, $message);
  346.                         return false;
  347.                 }
  348.  
  349.                 $logEvent = new OIDplusLogEvent($message);
  350.  
  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));
  354.                 }
  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));
  364.                                 }
  365.                         }
  366.  
  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));
  371.                                 $obj = OIDplusObject::parse($object_id);
  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));
  377.                                         }
  378.                                 } else {
  379.                                         //throw new OIDplusException(_L('%1 has no parent',$object_id));
  380.                                 }
  381.                         }
  382.  
  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];
  386.                                 $obj = OIDplusObject::parse($object_id);
  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)) {
  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) {
  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.                                                         }
  410.                                                 }
  411.                                         }
  412.                                 }
  413.                         }
  414.  
  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];
  418.                                 $obj = OIDplusObject::parse($object_id);
  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)) {
  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) {
  435.                                                         if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
  436.                                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra->raEmail()));
  437.                                                         }
  438.                                                 } else {
  439.                                                         if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
  440.                                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra->raEmail()));
  441.                                                         }
  442.                                                 }
  443.                                         }
  444.                                 }
  445.                         }
  446.  
  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.                                 }
  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) {
  463.                                         if (($severity_online_int = self::convertSeverity($severity_online)) >= 0) {
  464.                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity_online_int, $ra_email));
  465.                                         }
  466.                                 } else {
  467.                                         if (($severity_offline_int = self::convertSeverity($severity_offline)) >= 0) {
  468.                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity_offline_int, $ra_email));
  469.                                         }
  470.                                 }
  471.                         }
  472.  
  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];
  481.                                 }
  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) {
  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.                                 }
  496.                         }
  497.  
  498.                         // Unexpected
  499.                         else {
  500.                                 throw new OIDplusException(_L('Unexpected logger component type "%1" in mask code "%2"',$target[0],$maskcode));
  501.                         }
  502.                 }
  503.  
  504.                 // Now write the log message
  505.  
  506.                 $result = false;
  507.  
  508.                 if (count($logEvent->getTargets()) > 0) { // <-- count(targets)=0 for example of OIDRA(%1) gets notified during delete, but the object has no RA
  509.                         foreach ($loggerPlugins as $plugin) {
  510.                                 $reason = '';
  511.                                 if ($plugin->available($reason)) {
  512.                                         $result |= $plugin->log($logEvent);
  513.                                 }
  514.                         }
  515.                 }
  516.  
  517.                 return $result;
  518.         }
  519. }
  520.