Skip to content

Commit

Permalink
Email JSChart images with WWW::Mechanize::Chrome
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bconry-bps authored and sunnavy committed Dec 14, 2023
1 parent 2ee360a commit 541daa8
Show file tree
Hide file tree
Showing 5 changed files with 205 additions and 4 deletions.
23 changes: 23 additions & 0 deletions docs/UPGRADING-5.0
Original file line number Diff line number Diff line change
Expand Up @@ -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<WWW::Mechanize::Chrome> 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
41 changes: 41 additions & 0 deletions etc/RT_Config.pm.in
Original file line number Diff line number Diff line change
Expand Up @@ -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<WWW::Mechanize::Chrome> 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<WWW::Mechanize::Chrome/launch_exe>

=cut

Set($ChromePath, '');

=item C<@ChromeLaunchArguments>

This option contains the launch arguments when initializing
L<WWW::Mechanize::Chrome>.

If you need to run L<rt-email-dashboards> as root, you probably need to add
C<--no-sandbox> to get around Chrome's restriction:

Set(@ChromeLaunchArguments, '--no-sandbox');

See also L<WWW::Mechanize::Chrome/launch_arg>

=cut

Set(@ChromeLaunchArguments, () );


=back


Expand Down
9 changes: 9 additions & 0 deletions lib/RT/Config.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand Down
132 changes: 130 additions & 2 deletions lib/RT/Dashboard/Mailer.pm
Original file line number Diff line number Diff line change
Expand Up @@ -423,8 +423,6 @@ SUMMARY
}
}

$content = ScrubContent($content);

$RT::Logger->debug("Got ".length($content)." characters of output.");

$content = HTML::RewriteAttributes::Links->rewrite(
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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{<script type="text/javascript" src="([^"]+)"></script>}{<script type="text/javascript">@{ [(GetResource( $1 ))[0]] }</script>}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{<canvas [^>]+>}{<img src="cid:$cid"/>};

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{<link rel="shortcut icon"[^>]+/>}{};

# Inline the CSS if CSS::Inliner is installed and can be loaded
if ( RT->Config->Get('EmailDashboardInlineCSS') ) {
if ( RT::StaticUtil::RequireModule('CSS::Inliner') ) {
Expand All @@ -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}),
Expand Down
4 changes: 2 additions & 2 deletions sbin/rt-email-dashboards.in
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down

0 comments on commit 541daa8

Please sign in to comment.