Return GraphImage to include more metadata (#14307)

* Return GraphImage to include more metadata
Allows things like including title.
Implements __toString for backwards compatability
getImageData to allow controlling the output through flags

* Style and Lint
This commit is contained in:
Tony Murray
2022-09-06 07:33:57 -05:00
committed by GitHub
parent 69d1c2022a
commit 9fdc213f25
5 changed files with 191 additions and 64 deletions

View File

@@ -0,0 +1,91 @@
<?php
/**
* GraphImage.php
*
* Wrapper around a graph image to include metadata and control output format
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
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();
}
}

View File

@@ -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 = "<div class='infobox'>";
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Command</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= escapeshellcmd('rrdtool ' . Rrd::buildCommand('graph', Config::get('temp_dir') . '/' . strgen(), $rrd_options));
$cmd_output .= '</pre>';
try {
$cmd_output .= Rrd::graph($rrd_options);
} catch (RrdGraphException $e) {
$cmd_output .= "<p style='font-size: 16px; font-weight: bold;'>RRDTool Output</p>";
$cmd_output .= "<pre class='rrd-pre'>";
$cmd_output .= $e->getMessage();
$cmd_output .= '</pre>';
}
$cmd_output .= '</div>';
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 '<img src="data:' . self::imageType($graph_image_type) . ';base64,' . base64_encode($image_data) . '" alt="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';
}

View File

@@ -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('#<img class=\"librenms-graph\" src=\"(.*?)\" ?/?>#', $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
// 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);
}

View File

@@ -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']));
}
try {
$graph = Graph::get($vars);
if (Debug::isEnabled()) {
return response('<img src="' . $graph->inline() . '" alt="graph" />');
}
$headers = [
'Content-type' => Graph::imageType($vars['graph_type']),
'Content-type' => $graph->imageType(),
];
try {
return response(Graph::get($vars), 200, Debug::isEnabled() ? [] : $headers);
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()]);
}
}
}

View File

@@ -81,7 +81,7 @@ class AppServiceProvider extends ServiceProvider
});
Blade::directive('graphImage', function ($vars, $flags = 0) {
return "<?php echo \LibreNMS\Util\Graph::get($vars, $flags); ?>";
return "<?php echo \LibreNMS\Util\Graph::getImageData($vars, $flags); ?>";
});
}