From 541daa87378e8c9bb1f6730a829ae374addfd73e Mon Sep 17 00:00:00 2001 From: Brian Conry Date: Thu, 6 Oct 2022 13:01:35 -0500 Subject: [PATCH] Email JSChart images with WWW::Mechanize::Chrome This change allows the dashboard emails to contain image versions of JSChart graphs obtained using WWW::Mechanize::Chrome. Previously it was only possible to generate graph images for emails using the GD module. This Feature has been tested with Chrome, Chromium, Microsoft Edge, and Opera. --- docs/UPGRADING-5.0 | 23 +++++++ etc/RT_Config.pm.in | 41 +++++++++++ lib/RT/Config.pm | 9 +++ lib/RT/Dashboard/Mailer.pm | 132 +++++++++++++++++++++++++++++++++++- sbin/rt-email-dashboards.in | 4 +- 5 files changed, 205 insertions(+), 4 deletions(-) diff --git a/docs/UPGRADING-5.0 b/docs/UPGRADING-5.0 index d12cb26e195..4015f71d019 100644 --- a/docs/UPGRADING-5.0 +++ b/docs/UPGRADING-5.0 @@ -634,4 +634,27 @@ messages, you may need to update your system to match the new format. =back +=head1 UPGRADING FROM 5.0.5 AND EARLIER + +=over 4 + +=item * Additional options for charts in dashboard emails + +While it has been possible to use JSChart to generate chart images in the RT UI, +because these images are generated client-side it hasn't been possible to include +them in dashboard emails, so the GD-generated images have been the only option. + +It is now possible to use the optional Perl module L and +a compatible server-side web brwoser to create images of the JSChart graphs for +inclusion in emails. + +This is accomplished by setting C<$EmailDashboardJSChartImages> to '1' and +maybe also setting C<$ChromePath> to the fully-qualified path of the executable +for your chosen Chrome-based browser. + +This feature has been tested with Chrome, Chromium, Microsoft Edge, and Opera. +Other Chrome-based browsers may also work. + +=back + =cut diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in index 6433198a36b..c1cef7c71c6 100644 --- a/etc/RT_Config.pm.in +++ b/etc/RT_Config.pm.in @@ -971,6 +971,47 @@ With this enabled, some parts of the email won't look exactly like RT. Set($EmailDashboardInlineCSS, 0); +=item C<$EmailDashboardJSChartImages> + +To use the JSChart-generated images in emailed dashboards, install the +optional module L and enable this option. + +=cut + +Set($EmailDashboardJSChartImages, 0); + +=item C<$ChromePath> + +This option contains the fully-qualified path for a compatible Chrome-based +browser executable that will be used to generate static images for JSChart +graphs for dashboard emails. + +Use this option to set the path to your executable if it is in a non-standard +location or if the executable has a non-standard name. + +See also L + +=cut + +Set($ChromePath, ''); + +=item C<@ChromeLaunchArguments> + +This option contains the launch arguments when initializing +L. + +If you need to run L as root, you probably need to add +C<--no-sandbox> to get around Chrome's restriction: + + Set(@ChromeLaunchArguments, '--no-sandbox'); + +See also L + +=cut + +Set(@ChromeLaunchArguments, () ); + + =back diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm index 04fbe3dcd5c..36c7b547dc3 100644 --- a/lib/RT/Config.pm +++ b/lib/RT/Config.pm @@ -1994,6 +1994,15 @@ our %META; EmailDashboardInlineCSS => { Widget => '/Widgets/Form/Boolean', }, + EmailDashboardJSChartImages => { + Widget => '/Widgets/Form/Boolean', + }, + ChromePath => { + Widget => '/Widgets/Form/String', + }, + ChromeLaunchArguments => { + Type => 'ARRAY', + }, DefaultErrorMailPrecedence => { Widget => '/Widgets/Form/String', }, diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm index 84ce29e1716..e5fe46e0f6f 100644 --- a/lib/RT/Dashboard/Mailer.pm +++ b/lib/RT/Dashboard/Mailer.pm @@ -423,8 +423,6 @@ SUMMARY } } - $content = ScrubContent($content); - $RT::Logger->debug("Got ".length($content)." characters of output."); $content = HTML::RewriteAttributes::Links->rewrite( @@ -536,6 +534,8 @@ sub EmailDashboard { $RT::Logger->debug("Done sending dashboard to ".$currentuser->Name." <$email>"); } +my $chrome; + sub BuildEmail { my $self = shift; my %args = ( @@ -592,6 +592,132 @@ sub BuildEmail { inline_imports => 1, ); + # This needs to be done after all of the CSS has been imported (by + # inline_css above, which is distinct from the work done by CSS::Inliner + # below) and before all of the scripts are scrubbed away. + if ( RT->Config->Get('EmailDashboardJSChartImages') ) { + if ( RT::StaticUtil::RequireModule("WWW::Mechanize::Chrome") ) { + + # WWW::Mechanize::Chrome uses Log::Log4perl and calls trace sometimes. + # Here we turn off trace and also mimic trace as debug. + local *Log::Dispatch::is_trace = sub {0}; + local *Log::Dispatch::trace = sub { + my $self = shift; + return $self->debug(@_); + }; + + my ( $width, $height ); + my @launch_arguments = RT->Config->Get('ChromeLaunchArguments'); + + for my $arg (@launch_arguments) { + if ( $arg =~ /^--window-size=(\d+)x(\d+)$/ ) { + $width = $1; + $height = $2; + last; + } + } + + $width ||= 2560; + $height ||= 1440; + + $chrome ||= WWW::Mechanize::Chrome->new( + autodie => 0, + headless => 1, + autoclose => 1, + separate_session => 1, + log => RT->Logger, + launch_arg => \@launch_arguments, + RT->Config->Get('ChromePath') ? ( launch_exe => RT->Config->Get('ChromePath') ) : (), + ); + + # copy the content + my $content_with_script = $content; + + # copy in the text of the linked js + $content_with_script + =~ s{}{}g; + + # write the complete content to a temp file + my $temp_fh = File::Temp->new( + UNLINK => 1, + TEMPLATE => 'email-dashboard-XXXXXX', + SUFFIX => '.html', + DIR => $RT::VarPath, # $chrome can't get the file if saved to /tmp + ); + print $temp_fh Encode::encode( 'UTF-8', $content_with_script ); + close $temp_fh; + + $chrome->viewport_size( { width => $width, height => $height } ); + $chrome->get_local( $temp_fh->filename ); + $chrome->wait_until_visible( selector => 'div.dashboard' ); + + # grab the list of canvas elements + my @canvases = $chrome->selector('div.chart canvas'); + if (@canvases) { + + my $max_extent = 0; + + # ... and their coordinates + foreach my $canvas_data (@canvases) { + my $coords = $canvas_data->{coords} = $chrome->element_coordinates($canvas_data); + if ( $max_extent < $coords->{top} + $coords->{height} ) { + $max_extent = int( $coords->{top} + $coords->{height} ) + 1; + } + } + + # make sure that all of them are "visible" in the headless instance + if ( $height < $max_extent ) { + $chrome->viewport_size( { width => $width, height => $max_extent } ); + } + + # capture the entire page as an image + my $page_image = $chrome->_content_as_png( undef, { width => $width, height => $height } )->get; + + my $cid = time() . $$; + foreach my $canvas_data (@canvases) { + $cid++; + + my $coords = $canvas_data->{coords}; + my $canvas_image = $page_image->crop( + left => $coords->{left}, + top => $coords->{top}, + width => $coords->{width}, + height => $coords->{height}, + ); + my $canvas_data; + $canvas_image->write( data => \$canvas_data, type => 'png' ); + + # replace each canvas in the original content with an image tag + $content =~ s{]+>}{}; + + push @parts, + MIME::Entity->build( + Top => 0, + Data => $canvas_data, + Type => 'image/png', + Encoding => 'base64', + Disposition => 'inline', + 'Content-Id' => "<$cid>", + ); + } + } + + # Shut down chrome if it's a test email from web UI, to reduce memory usage. + # Unset $chrome so next time it can re-create a new one. + if ( $args{Test} ) { + $chrome->close; + undef $chrome; + } + } + else { + RT->Logger->warn( + 'EmailDashboardJSChartImages is enabled but WWW::Mechanize::Chrome is not installed. Install WWW::Mechanize::Chrome to use this feature.' + ); + } + } + + $content =~ s{]+/>}{}; + # Inline the CSS if CSS::Inliner is installed and can be loaded if ( RT->Config->Get('EmailDashboardInlineCSS') ) { if ( RT::StaticUtil::RequireModule('CSS::Inliner') ) { @@ -609,6 +735,8 @@ sub BuildEmail { } } + $content = ScrubContent($content); + my $entity = MIME::Entity->build( From => Encode::encode("UTF-8", $args{From}), To => Encode::encode("UTF-8", $args{To}), diff --git a/sbin/rt-email-dashboards.in b/sbin/rt-email-dashboards.in index 4b3b0087823..8cfbdef3dd8 100644 --- a/sbin/rt-email-dashboards.in +++ b/sbin/rt-email-dashboards.in @@ -89,8 +89,8 @@ RT::LoadConfig(); # adjust logging to the screen according to options RT->Config->Set( LogToSTDERR => $opts{log} ) if $opts{log}; -# Disable JS chart as email clients don't support it -RT->Config->Set( EnableJSChart => 0 ); +# Disable JS chart unless EmailDashboardJSChartImages is true +RT->Config->Set( EnableJSChart => RT->Config->Get( 'EmailDashboardJSChartImages' ) ); # Disable inline editing as email clients don't support it RT->Config->Set( InlineEdit => 0 );