Subversion Repositories oidplus

Rev

Rev 1200 | Rev 1267 | Go to most recent revision | 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 function splits a mask code containing multiple components
  30.          * (delimited by '+' or '/') in single components
  31.          * It takes care that '+' and '/' inside brackets won't be used to split the codes
  32.          * Also, brackets can be escaped.
  33.          * The severity block (optional, must be standing in front of a component)
  34.          * is handled too. Inside the severity block, you may only use '/' to split components.
  35.          * The severity block will be implicitly repeated from the previous components if a component
  36.          * does not feature one.
  37.          *
  38.          * "[ERR]AAA(BBB)+CCC(DDD)"   ==> array(
  39.          *                                 array(array("ERR"),"AAA(BBB)"),
  40.          *                                 array(array("ERR"),"CCC(DDD)")
  41.          *                              )
  42.          * "[INFO]AAA(B+BB)+[WARN]CCC(DDD)"  ==> array(
  43.          *                                 array(array("INFO"),"AAA(B+BB)"),
  44.          *                                 array(array("WARN"),"CCC(DDD)")
  45.          *                              )
  46.          * "[?WARN/!OK] AAA(B\)BB)+CCC(DDD)" ==> array(
  47.          *                                 array(array("?WARN", "!OK"),"AAA(B\)BB)"),
  48.          *                                 array(array("?WARN", "!OK"),"CCC(DDD)")
  49.          *                              )
  50.          * @param string $maskcodes
  51.          * @return array|false
  52.          */
  53.         private function split_maskcodes(string $maskcodes) {
  54.                 $out = array();
  55.                 $sevs = array(); // Note: The severity block will repeat for the next components if not changed explicitly
  56.  
  57.                 $code = '';
  58.                 $sev = '';
  59.                 $bracket_level = 0;
  60.                 $is_escaping = false;
  61.                 $inside_severity_block = false;
  62.                 for ($i=0; $i<strlen($maskcodes); $i++) {
  63.                         $char = $maskcodes[$i];
  64.  
  65.                         if ($inside_severity_block) {
  66.                                 // Severity block (optional)
  67.                                 // e.g.  [?WARN/!OK] ==> $sevs = array("?WARN", "!OK")
  68.                                 if ($char == '\\') {
  69.                                         if ($is_escaping) {
  70.                                                 $is_escaping = false;
  71.                                                 $sev .= $char;
  72.                                         } else {
  73.                                                 $is_escaping = true;
  74.                                         }
  75.                                 }
  76.                                 else if ($char == '[') {
  77.                                         if ($is_escaping) {
  78.                                                 $is_escaping = false;
  79.                                         } else {
  80.                                                 $bracket_level++;
  81.                                         }
  82.                                         $sev .= $char;
  83.                                 }
  84.                                 else if ($char == ']') {
  85.                                         if ($is_escaping) {
  86.                                                 $is_escaping = false;
  87.                                                 $sev .= $char;
  88.                                         } else {
  89.                                                 $bracket_level--;
  90.                                                 if ($bracket_level < 0) return false;
  91.                                                 if ($bracket_level == 0) {
  92.                                                         $inside_severity_block = false;
  93.                                                         if ($sev != '') $sevs[] = $sev;
  94.                                                         $sev = '';
  95.                                                 } else {
  96.                                                         $sev .= $char;
  97.                                                 }
  98.                                         }
  99.                                 }
  100.                                 else if ((($char == '/')) && ($bracket_level == 1)) {
  101.                                         if ($is_escaping) {
  102.                                                 $is_escaping = false;
  103.                                                 $sev .= $char;
  104.                                         } else {
  105.                                                 if ($sev != '') $sevs[] = $sev;
  106.                                                 $sev = '';
  107.                                         }
  108.                                 } else {
  109.                                         if ($is_escaping) {
  110.                                                 // This would actually be an error, because we cannot escape this
  111.                                                 $is_escaping = false;
  112.                                                 $sev .= '\\' . $char;
  113.                                         } else {
  114.                                                 $sev .= $char;
  115.                                         }
  116.                                 }
  117.                         } else {
  118.                                 // Normal data (after the severity block)
  119.                                 if (($char == '[') && ($code == '')) {
  120.                                         $inside_severity_block = true;
  121.                                         $bracket_level++;
  122.                                         $sevs = array();
  123.                                 }
  124.                                 else if ($char == '\\') {
  125.                                         if ($is_escaping) {
  126.                                                 $is_escaping = false;
  127.                                                 $code .= $char;
  128.                                         } else {
  129.                                                 $is_escaping = true;
  130.                                         }
  131.                                 }
  132.                                 else if ($char == '(') {
  133.                                         if ($is_escaping) {
  134.                                                 $is_escaping = false;
  135.                                         } else {
  136.                                                 $bracket_level++;
  137.                                         }
  138.                                         $code .= $char;
  139.                                 }
  140.                                 else if ($char == ')') {
  141.                                         if ($is_escaping) {
  142.                                                 $is_escaping = false;
  143.                                         } else {
  144.                                                 $bracket_level--;
  145.                                                 if ($bracket_level < 0) return false;
  146.                                         }
  147.                                         $code .= $char;
  148.                                 }
  149.                                 else if ((($char == '+') || ($char == '/')) && ($bracket_level == 0)) {
  150.                                         if ($is_escaping) {
  151.                                                 $is_escaping = false;
  152.                                                 $code .= $char;
  153.                                         } else {
  154.                                                 if ($code != '') $out[] = array($sevs,$code);
  155.                                                 $code = '';
  156.                                         }
  157.                                 } else {
  158.                                         if ($is_escaping) {
  159.                                                 // This would actually be an error, because we cannot escape this
  160.                                                 $is_escaping = false;
  161.                                                 $code .= '\\' . $char;
  162.                                         } else {
  163.                                                 $code .= $char;
  164.                                         }
  165.                                 }
  166.                         }
  167.                 }
  168.                 if ($code != '') $out[] = array($sevs,$code);
  169.                 if ($inside_severity_block) return false;
  170.  
  171.                 return $out;
  172.         }
  173.  
  174.         private $missing_plugin_queue = array();
  175.  
  176.         /**
  177.          * @return bool
  178.          * @throws OIDplusException
  179.          */
  180.         public function reLogMissing(): bool {
  181.                 while (count($this->missing_plugin_queue) > 0) {
  182.                         $item = $this->missing_plugin_queue[0];
  183.                         if (!$this->log_internal($item[0], $item[1], false)) return false;
  184.                         array_shift($this->missing_plugin_queue);
  185.                 }
  186.                 return true;
  187.         }
  188.  
  189.         /**
  190.          * @param string $maskcodes A description of the mask-codes can be found in doc/developer_notes/logger_maskcodes.md
  191.          * @param string $message The message of the event
  192.          * @param mixed ...$sprintfArgs If used, %1..%n in $maskcodes and $message will be replaced, like _L() does.
  193.          * @return bool
  194.          * @throws OIDplusException
  195.          */
  196.         public function log(string $maskcodes, string $message, ...$sprintfArgs): bool {
  197.                 $this->reLogMissing(); // try to re-log failed requests
  198.  
  199.                 $maskcodes = my_vsprintf($maskcodes, $sprintfArgs);
  200.                 $message = my_vsprintf($message, $sprintfArgs);
  201.  
  202.                 if (strpos(str_replace('%%','',$maskcodes),'%') !== false) {
  203.                         throw new OIDplusException(_L('Unresolved wildcards in logging maskcode'));
  204.                 }
  205.  
  206.                 return $this->log_internal($maskcodes, $message, true);
  207.         }
  208.  
  209.         /**
  210.          * @param string $maskcodes
  211.          * @param string $message
  212.          * @param bool $allow_delayed_log
  213.          * @return bool
  214.          * @throws OIDplusException
  215.          */
  216.         private function log_internal(string $maskcodes, string $message, bool $allow_delayed_log): bool {
  217.                 $loggerPlugins = OIDplus::getLoggerPlugins();
  218.                 if (count($loggerPlugins) == 0) {
  219.                         // The plugin might not be initialized in OIDplus::init()
  220.                         // yet. Remember the log entries for later submission during
  221.                         // OIDplus::init();
  222.                         if ($allow_delayed_log) $this->missing_plugin_queue[] = array($maskcodes, $message);
  223.                         return false;
  224.                 }
  225.  
  226.                 // What is a mask code?
  227.                 // A mask code gives information about the log event:
  228.                 // 1. The severity (info, warning, error)
  229.                 // 2. In which logbook(s) the event shall be placed
  230.                 // Example:
  231.                 // The event would be:
  232.                 // "Person 'X' moves from house 'A' to house 'B'"
  233.                 // This event would affect the person X and the two houses,
  234.                 // so, instead of logging into 3 logbooks separately,
  235.                 // you would create a mask code that tells the system
  236.                 // to put the message into the logbooks of person X,
  237.                 // house A, and house B.
  238.  
  239.                 $logEvent = new OIDplusLogEvent($message);
  240.  
  241.                 // A mask code with multiple components is split into single codes
  242.                 // using '+' or '/', e.g. "OID(x)+RA(x)" would be split to "OID(x)" and "RA(x)"
  243.                 // which would result in the message being placed in the logbook of OID x,
  244.                 // and the logbook of the RA owning OID x.
  245.                 $maskcodes_ary = $this->split_maskcodes($maskcodes);
  246.                 if ($maskcodes_ary === false) {
  247.                         throw new OIDplusException(_L('Invalid maskcode "%1" (failed to split)',$maskcodes));
  248.                 }
  249.                 foreach ($maskcodes_ary as list($sevs,$maskcode)) {
  250.                         // At the beginning of each mask code, you must define a severity.
  251.                         // If you have a mask code with multiple components, you don't have to place the
  252.                         // severity for each component. You can just leave it at the beginning.
  253.                         // e.g. "[WARN]OID(x)+RA(x)" is equal to "[WARN]OID(x)+[WARN]RA(x)"
  254.                         // You can also put different severities for the components:
  255.                         // e.g. "[INFO]OID(x)+[WARN]RA(x)" would be a info for the OID, but a warning for the RA.
  256.                         // If you want to make the severity dependent on wheather the user is logged in or not,
  257.                         // prepend "?" or "!" and use '/' as delimiter
  258.                         // Example: "[?WARN/!OK]RA(x)" means: If RA is not logged in, it is a warning; if it is logged in, it is an success
  259.                         $severity = 0; // default severity = none
  260.                         $severity_online = 0;
  261.                         foreach ($sevs as $sev) {
  262.                                 switch (strtoupper($sev)) {
  263.                                         // [OK]   = Success
  264.                                         //          Numeric value: 1
  265.                                         //          Rule of thumb: YOU have done something and it was successful
  266.                                         case '?OK':
  267.                                                 $severity_online = 1;
  268.                                                 break;
  269.                                         case '!OK':
  270.                                         case  'OK':
  271.                                                 $severity = 1;
  272.                                                 break;
  273.                                         // [INFO] = Informational
  274.                                         //          Numeric value: 2
  275.                                         //          Rule of thumb: Someone else has done something (that affects you) and it was successful
  276.                                         case '?INFO':
  277.                                                 $severity_online = 2;
  278.                                                 break;
  279.                                         case '!INFO':
  280.                                         case  'INFO':
  281.                                                 $severity = 2;
  282.                                                 break;
  283.                                         // [WARN] = Warning
  284.                                         //          Numeric value: 3
  285.                                         //          Rule of thumb: Something happened (probably someone did something) and it affects you
  286.                                         case '?WARN':
  287.                                                 $severity_online = 3;
  288.                                                 break;
  289.                                         case '!WARN':
  290.                                         case  'WARN':
  291.                                                 $severity = 3;
  292.                                                 break;
  293.                                         // [ERR]  = Error
  294.                                         //          Numeric value: 4
  295.                                         //          Rule of thumb: Something failed (probably someone did something) and it affects you
  296.                                         case '?ERR':
  297.                                                 $severity_online = 4;
  298.                                                 break;
  299.                                         case '!ERR':
  300.                                         case  'ERR':
  301.                                                 $severity = 4;
  302.                                                 break;
  303.                                         // [CRIT] = Critical
  304.                                         //          Numeric value: 5
  305.                                         //          Rule of thumb: Something happened (probably someone did something) which is not an error,
  306.                                         //          but some critical situation (e.g. hardware failure), and it affects you
  307.                                         case '?CRIT':
  308.                                                 $severity_online = 5;
  309.                                                 break;
  310.                                         case '!CRIT':
  311.                                         case  'CRIT':
  312.                                                 $severity = 5;
  313.                                                 break;
  314.                                         default:
  315.                                                 throw new OIDplusException(_L('Invalid maskcode "%1" (Unknown severity "%2")',$maskcodes,$sev));
  316.                                 }
  317.                         }
  318.  
  319.                         // OID(x)       Save log entry into the logbook of: Object "x"
  320.                         $m = array();
  321.                         if (preg_match('@^OID\((.+)\)$@ismU', $maskcode, $m)) {
  322.                                 $object_id = $m[1];
  323.                                 $logEvent->addTarget(new OIDplusLogTargetObject($severity, $object_id));
  324.                                 if ($object_id == '') throw new OIDplusException(_L('OID logger mask requires OID'));
  325.                         }
  326.  
  327.                         // SUPOID(x)    Save log entry into the logbook of: Parent of object "x"
  328.                         else if (preg_match('@^SUPOID\((.+)\)$@ismU', $maskcode, $m)) {
  329.                                 $object_id         = $m[1];
  330.                                 if ($object_id == '') throw new OIDplusException(_L('SUPOID logger mask requires OID'));
  331.                                 $obj = OIDplusObject::parse($object_id);
  332.                                 if ($obj) {
  333.                                         if ($objParent = $obj->getParent()) {
  334.                                                 $parent = $objParent->nodeId();
  335.                                                 $logEvent->addTarget(new OIDplusLogTargetObject($severity, $parent));
  336.                                         } else {
  337.                                                 //throw new OIDplusException(_L('%1 has no parent',$object_id));
  338.                                         }
  339.                                 } else {
  340.                                         throw new OIDplusException(_L('SUPOID logger mask: Invalid object %1',$object_id));
  341.                                 }
  342.                         }
  343.  
  344.                         // OIDRA(x)?    Save log entry into the logbook of: Logged in RA of object "x"
  345.                         // Remove or replace "?" by "!" if the entity does not need to be logged in
  346.                         else if (preg_match('@^OIDRA\((.+)\)([\?\!])$@ismU', $maskcode, $m)) {
  347.                                 $object_id         = $m[1];
  348.                                 $ra_need_login     = $m[2] == '?';
  349.                                 if ($object_id == '') throw new OIDplusException(_L('OIDRA logger mask requires OID'));
  350.                                 $obj = OIDplusObject::parse($object_id);
  351.                                 if ($obj) {
  352.                                         if ($ra_need_login) {
  353.                                                 foreach (OIDplus::authUtils()->loggedInRaList() as $ra) {
  354.                                                         if ($obj->userHasWriteRights($ra)) $logEvent->addTarget(new OIDplusLogTargetUser($severity_online, $ra->raEmail()));
  355.                                                 }
  356.                                         } else {
  357.                                                 // $logEvent->addTarget(new OIDplusLogTargetUser($severity, $obj->getRa()->raEmail()));
  358.                                                 foreach (OIDplusRA::getAllRAs() as $ra) {
  359.                                                         if ($obj->userHasWriteRights($ra)) $logEvent->addTarget(new OIDplusLogTargetUser($severity, $ra->raEmail()));
  360.                                                 }
  361.                                         }
  362.                                 } else {
  363.                                         throw new OIDplusException(_L('OIDRA logger mask: Invalid object "%1"',$object_id));
  364.                                 }
  365.                         }
  366.  
  367.                         // SUPOIDRA(x)? Save log entry into the logbook of: Logged in RA that owns the superior object of "x"
  368.                         // Remove or replace "?" by "!" if the entity does not need to be logged in
  369.                         else if (preg_match('@^SUPOIDRA\((.+)\)([\?\!])$@ismU', $maskcode, $m)) {
  370.                                 $object_id         = $m[1];
  371.                                 $ra_need_login     = $m[2] == '?';
  372.                                 if ($object_id == '') throw new OIDplusException(_L('SUPOIDRA logger mask requires OID'));
  373.                                 $obj = OIDplusObject::parse($object_id);
  374.                                 if ($obj) {
  375.                                         if ($ra_need_login) {
  376.                                                 foreach (OIDplus::authUtils()->loggedInRaList() as $ra) {
  377.                                                         if ($obj->userHasParentalWriteRights($ra)) $logEvent->addTarget(new OIDplusLogTargetUser($severity_online, $ra->raEmail()));
  378.                                                 }
  379.                                         } else {
  380.                                                 if ($objParent = $obj->getParent()) {
  381.                                                         // $logEvent->addTarget(new OIDplusLogTargetUser($severity, $objParent->getRa()->raEmail()));
  382.                                                         foreach (OIDplusRA::getAllRAs() as $ra) {
  383.                                                                 if ($obj->userHasParentalWriteRights($ra)) $logEvent->addTarget(new OIDplusLogTargetUser($severity, $ra->raEmail()));
  384.                                                         }
  385.                                                 } else {
  386.                                                         //throw new OIDplusException(_L('%1 has no parent, therefore also no parent RA',$object_id));
  387.                                                 }
  388.                                         }
  389.                                 } else {
  390.                                         throw new OIDplusException(_L('SUPOIDRA logger mask: Invalid object "%1"',$object_id));
  391.                                 }
  392.                         }
  393.  
  394.                         // RA(x)?       Save log entry into the logbook of: Logged in RA "x"
  395.                         // Remove or replace "?" by "!" if the entity does not need to be logged in
  396.                         else if (preg_match('@^RA\((.*)\)([\?\!])$@ismU', $maskcode, $m)) {
  397.                                 $ra_email          = $m[1];
  398.                                 $ra_need_login     = $m[2] == '?';
  399.                                 if (!empty($ra_email)) {
  400.                                         if ($ra_need_login && OIDplus::authUtils()->isRaLoggedIn($ra_email)) {
  401.                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity_online, $ra_email));
  402.                                         } else if (!$ra_need_login) {
  403.                                                 $logEvent->addTarget(new OIDplusLogTargetUser($severity, $ra_email));
  404.                                         }
  405.                                 }
  406.                         }
  407.  
  408.                         // A?   Save log entry into the logbook of: A logged in admin
  409.                         // Remove or replace "?" by "!" if the entity does not need to be logged in
  410.                         else if (preg_match('@^A([\?\!])$@imU', $maskcode, $m)) {
  411.                                 $admin_need_login = $m[1] == '?';
  412.                                 if ($admin_need_login && OIDplus::authUtils()->isAdminLoggedIn()) {
  413.                                         $logEvent->addTarget(new OIDplusLogTargetUser($severity_online, 'admin'));
  414.                                 } else if (!$admin_need_login) {
  415.                                         $logEvent->addTarget(new OIDplusLogTargetUser($severity, 'admin'));
  416.                                 }
  417.                         }
  418.  
  419.                         // Unexpected
  420.                         else {
  421.                                 throw new OIDplusException(_L('Unexpected logger component "%1" in mask code "%2"',$maskcode,$maskcodes));
  422.                         }
  423.                 }
  424.  
  425.                 // Now write the log message
  426.  
  427.                 $result = false;
  428.  
  429.                 foreach ($loggerPlugins as $plugin) {
  430.                         $reason = '';
  431.                         if ($plugin->available($reason)) {
  432.                                 $result |= $plugin->log($logEvent);
  433.                         }
  434.                 }
  435.  
  436.                 return $result;
  437.         }
  438. }
  439.