diff --git a/LibreNMS/Alert/AlertUtil.php b/LibreNMS/Alert/AlertUtil.php index 49545aa913..3da6ab8fab 100644 --- a/LibreNMS/Alert/AlertUtil.php +++ b/LibreNMS/Alert/AlertUtil.php @@ -93,19 +93,19 @@ class AlertUtil $uids = []; foreach ($results as $result) { $tmp = null; - if (is_numeric($result['bill_id'])) { + if (isset($result['bill_id']) && is_numeric($result['bill_id'])) { $tmpa = dbFetchRows('SELECT user_id FROM bill_perms WHERE bill_id = ?', [$result['bill_id']]); foreach ($tmpa as $tmp) { $uids[$tmp['user_id']] = $tmp['user_id']; } } - if (is_numeric($result['port_id'])) { + if (isset($result['port_id']) && is_numeric($result['port_id'])) { $tmpa = dbFetchRows('SELECT user_id FROM ports_perms WHERE port_id = ?', [$result['port_id']]); foreach ($tmpa as $tmp) { $uids[$tmp['user_id']] = $tmp['user_id']; } } - if (is_numeric($result['device_id'])) { + if (isset($result['device_id']) && is_numeric($result['device_id'])) { if (Config::get('alert.syscontact') == true) { if (dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_bool' AND device_id = ?", [$result['device_id']])) { $tmpa = dbFetchCell("SELECT attrib_value FROM devices_attribs WHERE attrib_type = 'override_sysContact_string' AND device_id = ?", [$result['device_id']]); diff --git a/LibreNMS/Alert/Transport.php b/LibreNMS/Alert/Transport.php index c5edc3645f..6ba0bacdec 100644 --- a/LibreNMS/Alert/Transport.php +++ b/LibreNMS/Alert/Transport.php @@ -65,12 +65,12 @@ abstract class Transport implements TransportInterface * @param string $input * @return array */ - protected function parseUserOptions($input) + protected function parseUserOptions(string $input): array { $options = []; - foreach (explode(PHP_EOL, $input) as $option) { + foreach (preg_split('/\\r\\n|\\r|\\n/', $input, -1, PREG_SPLIT_NO_EMPTY) as $option) { if (Str::contains($option, '=')) { - [$k,$v] = explode('=', $option, 2); + [$k, $v] = explode('=', $option, 2); $options[$k] = trim($v); } } diff --git a/LibreNMS/Alert/Transport/Api.php b/LibreNMS/Alert/Transport/Api.php index b12c92fb9b..e84c78a6d1 100644 --- a/LibreNMS/Alert/Transport/Api.php +++ b/LibreNMS/Alert/Transport/Api.php @@ -24,6 +24,7 @@ namespace LibreNMS\Alert\Transport; +use App\View\SimpleTemplate; use LibreNMS\Alert\Transport; use LibreNMS\Util\Proxy; @@ -46,31 +47,14 @@ class Api extends Transport private function contactAPI($obj, $api, $options, $method, $auth, $headers, $body) { $request_opts = []; - $request_heads = []; - $query = []; $method = strtolower($method); $host = explode('?', $api, 2)[0]; //we don't use the parameter part, cause we build it out of options. //get each line of key-values and process the variables for Headers; - foreach (preg_split('/\\r\\n|\\r|\\n/', $headers, -1, PREG_SPLIT_NO_EMPTY) as $current_line) { - [$u_key, $u_val] = explode('=', $current_line, 2); - foreach ($obj as $p_key => $p_val) { - $u_val = str_replace('{{ $' . $p_key . ' }}', $p_val, $u_val); - } - //store the parameter in the array for HTTP headers - $request_heads[$u_key] = $u_val; - } + $request_heads = $this->parseUserOptions(SimpleTemplate::parse($headers, $obj)); //get each line of key-values and process the variables for Options; - foreach (preg_split('/\\r\\n|\\r|\\n/', $options, -1, PREG_SPLIT_NO_EMPTY) as $current_line) { - [$u_key, $u_val] = explode('=', $current_line, 2); - // Replace the values - foreach ($obj as $p_key => $p_val) { - $u_val = str_replace('{{ $' . $p_key . ' }}', $p_val, $u_val); - } - //store the parameter in the array for HTTP query - $query[$u_key] = $u_val; - } + $query = $this->parseUserOptions(SimpleTemplate::parse($options, $obj)); $client = new \GuzzleHttp\Client(); $request_opts['proxy'] = Proxy::forGuzzle(); @@ -89,10 +73,7 @@ class Api extends Transport $res = $client->request('PUT', $host, $request_opts); } else { //Method POST $request_opts['query'] = $query; - foreach ($obj as $metric => $value) { - $body = str_replace('{{ $' . $metric . ' }}', $value, $body); - } - $request_opts['body'] = $body; + $request_opts['body'] = SimpleTemplate::parse($body, $obj); $res = $client->request('POST', $host, $request_opts); } diff --git a/LibreNMS/Alert/Transport/Matrix.php b/LibreNMS/Alert/Transport/Matrix.php index 6f6ef99b8b..8dc1d49489 100644 --- a/LibreNMS/Alert/Transport/Matrix.php +++ b/LibreNMS/Alert/Transport/Matrix.php @@ -23,6 +23,7 @@ namespace LibreNMS\Alert\Transport; +use App\View\SimpleTemplate; use LibreNMS\Alert\Transport; use LibreNMS\Util\Proxy; @@ -51,9 +52,7 @@ class Matrix extends Transport $request_heads['Content-Type'] = 'application/json'; $request_heads['Accept'] = 'application/json'; - foreach ($obj as $p_key => $p_val) { - $message = str_replace('{{ $' . $p_key . ' }}', $p_val, $message); - } + $message = SimpleTemplate::parse($message, $obj); $body = ['body'=>$message, 'msgtype'=>'m.text']; diff --git a/LibreNMS/Alert/Transport/Rocket.php b/LibreNMS/Alert/Transport/Rocket.php index e91dda8076..8655bf17af 100644 --- a/LibreNMS/Alert/Transport/Rocket.php +++ b/LibreNMS/Alert/Transport/Rocket.php @@ -28,6 +28,8 @@ use LibreNMS\Util\Proxy; class Rocket extends Transport { + protected $name = 'Rocket Chat'; + public function deliverAlert($obj, $opts) { $rocket_opts = $this->parseUserOptions($this->config['rocket-options']); @@ -51,10 +53,10 @@ class Rocket extends Transport 'text' => $rocket_msg, ], ], - 'channel' => $api['channel'], - 'username' => $api['username'], - 'icon_url' => $api['icon_url'], - 'icon_emoji' => $api['icon_emoji'], + 'channel' => $api['channel'] ?? null, + 'username' => $api['username'] ?? null, + 'icon_url' => $api['icon_url'] ?? null, + 'icon_emoji' => $api['icon_emoji'] ?? null, ]; $alert_message = json_encode($data); curl_setopt($curl, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); diff --git a/LibreNMS/Device/YamlDiscovery.php b/LibreNMS/Device/YamlDiscovery.php index d29d6c2641..ab21dcb194 100644 --- a/LibreNMS/Device/YamlDiscovery.php +++ b/LibreNMS/Device/YamlDiscovery.php @@ -25,6 +25,7 @@ namespace LibreNMS\Device; +use App\View\SimpleTemplate; use Cache; use Illuminate\Support\Arr; use Illuminate\Support\Str; @@ -166,35 +167,29 @@ class YamlDiscovery $value = static::getValueFromData($name, $index, $def, $pre_cache); if (is_null($value)) { - // built in replacements - $search = [ - '{{ $index }}', - '{{ $count }}', + // basic replacements + $variables = [ + 'index' => $index, + 'count' => $count, ]; - $replace = [ - $index, - $count, - ]; - - // prepare the $subindexX match variable replacement foreach (explode('.', $index) as $pos => $subindex) { - $search[] = '{{ $subindex' . $pos . ' }}'; - $replace[] = $subindex; + $variables['subindex' . $pos] = $subindex; } - - $value = str_replace($search, $replace, $def[$name] ?? ''); + $value = (string) (new SimpleTemplate($def[$name] ?? '', $variables))->keepEmptyTemplates(); // search discovery data for values - $value = preg_replace_callback('/{{ \$?([a-zA-Z0-9\-.:]+) }}/', function ($matches) use ($index, $def, $pre_cache) { - $replace = static::getValueFromData($matches[1], $index, $def, $pre_cache, null); + $template = new SimpleTemplate($value); + $template->replaceWith(function ($matches) use ($index, $def, $pre_cache) { + $replace = static::getValueFromData($matches[1], $index, $def, $pre_cache); if (is_null($replace)) { - d_echo('Warning: No variable available to replace ' . $matches[1] . ".\n"); + \Log::warning('YamlDiscovery: No variable available to replace ' . $matches[1]); return ''; // remove the unavailable variable } return $replace; - }, $value); + }); + $value = (string) $template; } return $value; diff --git a/LibreNMS/OS/Traits/YamlOSDiscovery.php b/LibreNMS/OS/Traits/YamlOSDiscovery.php index 55cef3999d..894c26e3f4 100644 --- a/LibreNMS/OS/Traits/YamlOSDiscovery.php +++ b/LibreNMS/OS/Traits/YamlOSDiscovery.php @@ -27,6 +27,7 @@ namespace LibreNMS\OS\Traits; use App\Models\Device; use App\Models\Location; +use App\View\SimpleTemplate; use Illuminate\Support\Arr; use LibreNMS\Util\StringHelpers; use Log; @@ -75,7 +76,7 @@ trait YamlOSDiscovery } $device->$field = isset($os_yaml["{$field}_template"]) - ? $this->parseTemplate($os_yaml["{$field}_template"], $data) + ? trim(SimpleTemplate::parse($os_yaml["{$field}_template"], $data)) : $value; } } @@ -131,13 +132,6 @@ trait YamlOSDiscovery } } - private function parseTemplate($template, $data) - { - return trim(preg_replace_callback('/{{ ([^ ]+) }}/', function ($matches) use ($data) { - return $data[$matches[1]] ?? ''; - }, $template)); - } - private function translateSysObjectID($mib, $regex) { $device = $this->getDevice(); diff --git a/LibreNMS/Util/StringHelpers.php b/LibreNMS/Util/StringHelpers.php index 97edbf04e4..bb77bac020 100644 --- a/LibreNMS/Util/StringHelpers.php +++ b/LibreNMS/Util/StringHelpers.php @@ -146,4 +146,15 @@ class StringHelpers return $namespace . $class; } + + /** + * Check if variable can be cast to a string + * + * @param mixed $var + * @return bool + */ + public static function isStringable($var): bool + { + return $var === null || is_scalar($var) || (is_object($var) && method_exists($var, '__toString')); + } } diff --git a/app/Http/Controllers/AlertTransportController.php b/app/Http/Controllers/AlertTransportController.php index 771602faf1..b3da96548b 100644 --- a/app/Http/Controllers/AlertTransportController.php +++ b/app/Http/Controllers/AlertTransportController.php @@ -44,7 +44,8 @@ class AlertTransportController extends Controller return response()->json(['status' => 'ok']); } } catch (\Exception $e) { - $result = $e->getMessage(); + \Log::error($e); + $result = basename($e->getFile(), '.php') . ':' . $e->getLine() . ' ' . $e->getMessage(); } return response()->json([ diff --git a/app/View/SimpleTemplate.php b/app/View/SimpleTemplate.php new file mode 100644 index 0000000000..5781744930 --- /dev/null +++ b/app/View/SimpleTemplate.php @@ -0,0 +1,113 @@ +. + * + * @package LibreNMS + * @link http://librenms.org + * @copyright 2021 Tony Murray + * @author Tony Murray + */ + +namespace App\View; + +use LibreNMS\Util\StringHelpers; + +class SimpleTemplate +{ + /** + * @var string + */ + private $template; + /** + * @var array + */ + private $variables; + /** + * @var string + */ + private $regex = '/{{ \$?([a-zA-Z0-9\-_.:]+) }}/'; + /** + * @var callable + */ + private $callback; + /** + * @var bool + */ + private $keepEmpty = false; + + public function __construct(string $template, array $variables = []) + { + $this->template = $template; + $this->variables = $variables; + } + + /** + * By default, unmatched templates will be removed from the output, set this to keep them + */ + public function keepEmptyTemplates(): SimpleTemplate + { + $this->keepEmpty = true; + + return $this; + } + + /** + * Add a variable to the set of possible substitutions + */ + public function setVariable(string $key, string $value): SimpleTemplate + { + $this->variables[$key] = $value; + + return $this; + } + + /** + * Instead of using the given variables to replace {{ var }} + * send the matched variable to this callback, which will return a string to replace it + */ + public function replaceWith(callable $callback): SimpleTemplate + { + $this->callback = $callback; + + return $this; + } + + /** + * Create and parse a simple template + * + * @param string $template + * @param array $variables + * @return string + */ + public static function parse(string $template, array $variables): string + { + return (string) new static($template, $variables); + } + + public function __toString() + { + return preg_replace_callback($this->regex, $this->callback ?? function ($matches) { + $replacement = $this->variables[$matches[1]] ?? ($this->keepEmpty ? $matches[0] : ''); + if (! StringHelpers::isStringable($replacement)) { + return ''; + } + + return $replacement; + }, $this->template); + } +} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c5d10e3e43..8cfd5c65de 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -3820,21 +3820,6 @@ parameters: count: 1 path: LibreNMS/OS.php - - - message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#" - count: 1 - path: LibreNMS/OS.php - - - - message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS.php - - - - message: "#^Method LibreNMS\\\\OS\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS.php - - message: "#^Method LibreNMS\\\\OS\\:\\:persistGraphs\\(\\) has no return typehint specified\\.$#" count: 1 @@ -4375,21 +4360,6 @@ parameters: count: 1 path: LibreNMS/OS/Shared/Cisco.php - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Cisco.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Cisco.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Cisco.php - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Cisco\\:\\:pollSlas\\(\\) has no return typehint specified\\.$#" count: 1 @@ -4530,21 +4500,6 @@ parameters: count: 1 path: LibreNMS/OS/Shared/Unix.php - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Unix.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Unix.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Unix.php - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Unix\\:\\:translateSysObjectID\\(\\) has no return typehint specified\\.$#" count: 1 @@ -4625,21 +4580,6 @@ parameters: count: 1 path: LibreNMS/OS/Shared/Zyxel.php - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has no return typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Zyxel.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has parameter \\$data with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Zyxel.php - - - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:parseTemplate\\(\\) has parameter \\$template with no typehint specified\\.$#" - count: 1 - path: LibreNMS/OS/Shared/Zyxel.php - - message: "#^Method LibreNMS\\\\OS\\\\Shared\\\\Zyxel\\:\\:translateSysObjectID\\(\\) has no return typehint specified\\.$#" count: 1 diff --git a/tests/Unit/Util/StringHelperTest.php b/tests/Unit/Util/StringHelperTest.php index 9bf0904c84..0c491b83d2 100644 --- a/tests/Unit/Util/StringHelperTest.php +++ b/tests/Unit/Util/StringHelperTest.php @@ -35,7 +35,7 @@ class StringHelperTest extends TestCase * * @return void */ - public function testInferEncoding() + public function testInferEncoding(): void { $this->assertEquals(null, StringHelpers::inferEncoding(null)); $this->assertEquals('', StringHelpers::inferEncoding('')); @@ -48,4 +48,31 @@ class StringHelperTest extends TestCase config(['app.charset' => 'Shift_JIS']); $this->assertEquals('コンサート', StringHelpers::inferEncoding(base64_decode('g1KDk4NUgVuDZw=='))); } + + public function testIsStringable(): void + { + $this->assertTrue(StringHelpers::isStringable(null)); + $this->assertTrue(StringHelpers::isStringable('')); + $this->assertTrue(StringHelpers::isStringable('string')); + $this->assertTrue(StringHelpers::isStringable(-1)); + $this->assertTrue(StringHelpers::isStringable(1.0)); + $this->assertTrue(StringHelpers::isStringable(false)); + + $this->assertFalse(StringHelpers::isStringable([])); + $this->assertFalse(StringHelpers::isStringable((object) [])); + + $stringable = new class + { + public function __toString() + { + return ''; + } + }; + $this->assertTrue(StringHelpers::isStringable($stringable)); + + $nonstringable = new class + { + }; + $this->assertFalse(StringHelpers::isStringable($nonstringable)); + } }