diff --git a/LibreNMS/Data/Graphing/GraphImage.php b/LibreNMS/Data/Graphing/GraphImage.php new file mode 100644 index 0000000000..26ab7cb84f --- /dev/null +++ b/LibreNMS/Data/Graphing/GraphImage.php @@ -0,0 +1,91 @@ +. + * + * @link https://www.librenms.org + * + * @copyright 2022 Tony Murray + * @author Tony Murray + */ + +namespace LibreNMS\Data\Graphing; + +class GraphImage +{ + /** + * @var string + */ + private $type; + /** + * @var string + */ + private $title; + /** + * @var string + */ + private $data; + + public function __construct(string $type, string $title, string $data) + { + $this->type = $type; + $this->title = $title; + $this->data = $data; + } + + public function title(): string + { + return $this->title; + } + + public function data(): string + { + return $this->data; + } + + public function base64(): string + { + return base64_encode($this->data); + } + + public function inline(): string + { + return 'data:' . $this->imageType() . ';base64,' . $this->base64(); + } + + public function fileExtension(): string + { + switch ($this->imageType()) { + case 'image/svg+xml': + return 'svg'; + case 'image/png': + // fallthrough + default: + return 'png'; + } + } + + public function imageType(): string + { + return $this->type; + } + + public function __toString() + { + return $this->data(); + } +} diff --git a/LibreNMS/Util/Graph.php b/LibreNMS/Util/Graph.php index c40256c99d..0ed05f84e4 100644 --- a/LibreNMS/Util/Graph.php +++ b/LibreNMS/Util/Graph.php @@ -29,6 +29,7 @@ use App\Facades\DeviceCache; use App\Models\Device; use Illuminate\Support\Facades\Auth; use LibreNMS\Config; +use LibreNMS\Data\Graphing\GraphImage; use LibreNMS\Exceptions\RrdGraphException; use Rrd; @@ -36,19 +37,66 @@ class Graph { const BASE64_OUTPUT = 1; // BASE64 encoded image data const INLINE_BASE64 = 2; // img src inline base64 image - const COMMAND_ONLY = 4; // just print the command + const IMAGE_PNG = 4; // img src inline base64 image + const IMAGE_SVG = 8; // img src inline base64 image /** - * Fetch a graph image (as string) based on the given $vars - * Optionally, override the output format to base64 + * Convenience helper to specify desired image output * * @param array|string $vars - * @param int $flags Flags for controlling graph generating options. + * @param int $flags * @return string + */ + public static function getImageData($vars, int $flags = 0): string + { + if ($flags & self::IMAGE_PNG) { + $vars['graph_type'] = 'png'; + } + + if ($flags & self::IMAGE_SVG) { + $vars['graph_type'] = 'svg'; + } + + if ($flags & self::INLINE_BASE64) { + return self::getImage($vars)->inline(); + } + + if ($flags & self::BASE64_OUTPUT) { + return self::getImage($vars)->base64(); + } + + return self::getImage($vars)->data(); + } + + /** + * Fetch a GraphImage based on the given $vars + * Catches errors generated and always returns GraphImage + * + * @param array|string $vars + * @return GraphImage + */ + public static function getImage($vars): GraphImage + { + try { + return self::get($vars); + } catch (RrdGraphException $e) { + if (Debug::isEnabled()) { + throw $e; + } + + return new GraphImage(self::imageType(), 'Error', $e->generateErrorImage()); + } + } + + /** + * Fetch a GraphImage based on the given $vars + * + * @param array|string $vars + * @return GraphImage * * @throws \LibreNMS\Exceptions\RrdGraphException */ - public static function get($vars, int $flags = 0): string + public static function get($vars): GraphImage { define('IGNORE_ERRORS', true); chdir(base_path()); @@ -80,13 +128,11 @@ class Graph $title = $vars['title'] ?? ''; $vertical = $vars['vertical'] ?? ''; $legend = $vars['legend'] ?? false; - $output = $vars['output'] ?? 'default'; $from = parse_at_time($vars['from'] ?? '-1d'); $to = empty($vars['to']) ? time() : parse_at_time($vars['to']); $period = ($to - $from); $prev_from = ($from - $period); $graph_image_type = $vars['graph_type'] ?? Config::get('webui.graph_type'); - Config::set('webui.graph_type', $graph_image_type); // set in case accessed elsewhere $rrd_options = ''; $rrd_filename = null; @@ -113,26 +159,6 @@ class Graph } } - // command output requested - if ($flags & self::COMMAND_ONLY) { - $cmd_output = "
"; - $cmd_output .= "

RRDTool Command

"; - $cmd_output .= "
";
-            $cmd_output .= escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options));
-            $cmd_output .= '
'; - try { - $cmd_output .= Rrd::graph($rrd_options); - } catch (RrdGraphException $e) { - $cmd_output .= "

RRDTool Output

"; - $cmd_output .= "
";
-                $cmd_output .= $e->getMessage();
-                $cmd_output .= '
'; - } - $cmd_output .= '
'; - - return $cmd_output; - } - if (empty($rrd_options)) { throw new RrdGraphException('Graph Definition Error', 'Def Error', $width, $height); } @@ -141,16 +167,7 @@ class Graph try { $image_data = Rrd::graph($rrd_options); - // output the graph int the desired format - if (Debug::isEnabled()) { - return 'graph'; - } elseif ($flags & self::BASE64_OUTPUT || $output == 'base64') { - return base64_encode($image_data); - } elseif ($flags & self::INLINE_BASE64 || $output == 'inline-base64') { - return 'data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data); - } - - return $image_data; // raw data + return new GraphImage(self::imageType($graph_image_type), $title ?? $graph_title, $image_data); } catch (RrdGraphException $e) { // preserve original error if debug is enabled, otherwise make it a little more user friendly if (Debug::isEnabled()) { @@ -237,11 +254,15 @@ class Graph /** * Get the http content type of the image * - * @param string $type svg or png + * @param string|null $type svg or png * @return string */ - public static function imageType(string $type): string + public static function imageType(?string $type = null): string { + if ($type === null) { + $type = Config::get('webui.graph_type'); + } + return $type === 'svg' ? 'image/svg+xml' : 'image/png'; } diff --git a/LibreNMS/Util/Mail.php b/LibreNMS/Util/Mail.php index 738af32c18..6cd74c46cc 100644 --- a/LibreNMS/Util/Mail.php +++ b/LibreNMS/Util/Mail.php @@ -91,7 +91,7 @@ class Mail $mail->WordWrap = 76; $mail->Body = $message; if ($embedGraphs ?? Config::get('email_attach_graphs')) { - self::embedGraphs($mail); + self::embedGraphs($mail, $html); } if ($html) { $mail->isHTML(); @@ -132,32 +132,37 @@ class Mail /** * Search for generated graph links, generate them, attach them to the email and update the url to a cid link */ - private static function embedGraphs(PHPMailer $mail): void + private static function embedGraphs(PHPMailer $mail, bool $html = false): void { $body = $mail->Body; // search for generated graphs - preg_match_all('/ class=\"librenms-graph\" src=\"(.*?)\"/', $body, $match); + preg_match_all('##', $body, $matches); - foreach (array_values(array_unique($match[1])) as $attachment_id => $url) { + $count = 0; + foreach (array_combine($matches[1], $matches[0]) as $url => $tag) { try { - $cid = "graph$attachment_id"; + $cid = 'graph' . ++$count; - // fetch image, do not debug as it will return the wrong format. - $prev = Debug::isEnabled(); - Debug::set(false); - $image = Graph::get(Url::parseLegacyPathVars($url)); - Debug::set($prev); + // fetch image data + $image = Graph::getImage($url); // attach image - if (Config::get('webui.graph_type') == 'svg') { - $mail->addStringEmbeddedImage($image, $cid, "$cid.svg", PHPMailer::ENCODING_BASE64, 'image/svg+xml'); - } else { - $mail->addStringEmbeddedImage($image, $cid, "$cid.png", PHPMailer::ENCODING_BASE64, 'image/png'); - } + $fileName = substr(Clean::fileName($image->title() ?: $cid), 0, 250); + $mail->addStringEmbeddedImage( + $image->data(), + $cid, + $fileName . '.' . $image->fileExtension(), + PHPMailer::ENCODING_BASE64, + $image->imageType() + ); - // update image tag to link to attached image - $body = str_replace($url, "cid:$cid", $body); + // update image tag to link to attached image, or just the image name + if ($html) { + $body = str_replace($url, "cid:$cid", $body); + } else { + $body = str_replace($tag, "[$fileName]", $body); + } } catch (RrdGraphException|\PHPMailer\PHPMailer\Exception $e) { report($e); } diff --git a/app/Http/Controllers/GraphController.php b/app/Http/Controllers/GraphController.php index a73d06e283..9ead73a55b 100644 --- a/app/Http/Controllers/GraphController.php +++ b/app/Http/Controllers/GraphController.php @@ -18,25 +18,35 @@ class GraphController extends Controller public function __invoke(Request $request, string $path = ''): Response { $vars = array_merge(Url::parseLegacyPathVars($request->path()), $request->except(['username', 'password'])); - $vars['graph_type'] = $vars['graph_type'] ?? Config::get('webui.graph_type'); + $output = $vars['graph_type'] ?? Config::get('webui.graph_type', 'default'); if (\Auth::check()) { // only allow debug for logged in users Debug::set(! empty($vars['debug'])); } - $headers = [ - 'Content-type' => Graph::imageType($vars['graph_type']), - ]; - try { - return response(Graph::get($vars), 200, Debug::isEnabled() ? [] : $headers); + $graph = Graph::get($vars); + + if (Debug::isEnabled()) { + return response('graph'); + } + + $headers = [ + 'Content-type' => $graph->imageType(), + ]; + + if ($output == 'base64') { + return response($graph, 200, $headers); + } + + return response($graph->data(), 200, $headers); } catch (RrdGraphException $e) { if (Debug::isEnabled()) { throw $e; } - return response($e->generateErrorImage(), 500, $headers); + return response($e->generateErrorImage(), 500, ['Content-type' => Graph::imageType()]); } } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 63b69a967b..0b259f2378 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider }); Blade::directive('graphImage', function ($vars, $flags = 0) { - return ""; + return ""; }); }