[Rt-commit] rt branch 5.0/create-chart-images-for-dashboard-emails created. rt-5.0.3-87-g203a5c730a

BPS Git Server git at git.bestpractical.com
Fri Oct 7 13:03:21 UTC 2022


This is an automated email from the git hooks/post-receive script. It was
generated because a ref change was pushed to the repository containing
the project "rt".

The branch, 5.0/create-chart-images-for-dashboard-emails has been created
        at  203a5c730ab537dbf87a1201907dd6cd7c77cc5e (commit)

- Log -----------------------------------------------------------------
commit 203a5c730ab537dbf87a1201907dd6cd7c77cc5e
Author: Brian Conry <bconry at bestpractical.com>
Date:   Thu Oct 6 13:01:35 2022 -0500

    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.

diff --git a/docs/UPGRADING-5.0 b/docs/UPGRADING-5.0
index 9991928fcd..4df7f6f3f5 100644
--- a/docs/UPGRADING-5.0
+++ b/docs/UPGRADING-5.0
@@ -487,6 +487,23 @@ additional defense against CSRF attacks in some browsers.  See
 L<https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite>
 for more details on valid values, their meaning, and browser support.
 
+=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 C<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 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 60dbbf0c96..23aadcbc9b 100644
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -907,6 +907,29 @@ 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, you can install the
+optional module C<WWW::Mechanize::Chrome>, set this option, and set
+C<$ChromePath> to the executable for a compatible Chrome-based browser.
+
+=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.
+
+This has been confirmed to also work with Chromium, Microsoft Edge, and Opera.
+
+=cut
+
+Set($ChromePath, '');
+
+
 =back
 
 
diff --git a/lib/RT/Config.pm b/lib/RT/Config.pm
index 0259ff7996..85bf6bceea 100644
--- a/lib/RT/Config.pm
+++ b/lib/RT/Config.pm
@@ -1897,6 +1897,12 @@ our %META;
     EmailDashboardInlineCSS => {
         Widget => '/Widgets/Form/Boolean',
     },
+    EmailDashboardJSChartImages => {
+        Widget => '/Widgets/Form/Boolean',
+    },
+    ChromePath => {
+        Widget => '/Widgets/Form/String',
+    },
     DefaultErrorMailPrecedence => {
         Widget => '/Widgets/Form/String',
     },
diff --git a/lib/RT/Dashboard/Mailer.pm b/lib/RT/Dashboard/Mailer.pm
index ae642d3905..a5b4a706fc 100644
--- a/lib/RT/Dashboard/Mailer.pm
+++ b/lib/RT/Dashboard/Mailer.pm
@@ -361,8 +361,6 @@ SUMMARY
         }
     }
 
-    $content = ScrubContent($content);
-
     $RT::Logger->debug("Got ".length($content)." characters of output.");
 
     $content = HTML::RewriteAttributes::Links->rewrite(
@@ -530,6 +528,115 @@ 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 ( WWW::Mechanize::Chrome->require ) {
+            my $width = 1920;
+            my $height = 1080;
+CHROMELOOP:    {
+            require GD::Image;
+
+            # 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>}sg;
+
+            # TODO: make chrome instance persist across multiple dashboards
+            # start chrome
+            my $chrome_data_dir = tempdir(CLEANUP => 1);
+            my $chrome = WWW::Mechanize::Chrome->new(
+                autodie => 0,
+                headless => 1,
+                autoclose => 1,
+                data_directory => $chrome_data_dir,
+                profile_directory => $chrome_data_dir . '/profile',
+                launch_exe => '/usr/bin/chromium',
+                # the width is most important here
+                launch_arg => [ "--window-size=${width}x$height" ],
+            );
+            sleep 1;
+
+            $content_with_script = Encode::encode( "UTF-8", $content_with_script );
+
+            # write the complete content to a temp file
+            my $temp_fh = File::Temp->new( UNLINK => 1, SUFFIx => '.html' );
+            print $temp_fh $content_with_script;
+            close $temp_fh;
+
+            # load the file in chrome
+            $chrome->get_local( $temp_fh->filename );
+            $chrome->wait_until_visible( selector => 'div.dashboard' );
+            sleep 1;
+
+            # grab the list of canvas elements
+            my @canvases = map { { element => $_ } } $chrome->selector('div.chart canvas');
+
+            last CHROMELOOP unless @canvases;
+
+            my $max_extent = 0;
+
+            # ... and their coordinates
+            foreach my $canvas_data ( @canvases ) {
+                my $coords = $canvas_data->{coords} = $chrome->element_coordinates( $canvas_data->{element} );
+                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 ) {
+                $height = $max_extent;
+                redo CHROMELOOP;
+            }
+
+            # capture the entire page as an image
+            my $page_image = GD::Image->newFromPngData( $chrome->render_content( format => 'png' ) );
+
+            foreach my $canvas_data ( @canvases ) {
+                my $coords = $canvas_data->{coords};
+
+                my $canvas_image = GD::Image->new( $coords->{width}, $coords->{height} );
+
+                # cut out each canvas by coordinates
+                $canvas_image->copy(
+                    # source image
+                    $page_image,
+                    # location in destination image
+                    0, 0,
+                    # location in destination image
+                    $coords->{left}, $coords->{top}, 
+                    # size
+                    $coords->{width}, $coords->{height},
+                );
+
+                my $cid = time() . $$ . int(rand(1e6));
+
+                # replace each canvas in the original content with an image tag
+                $content =~ s{<canvas [^>]+>}{<img src="cid:$cid"/>}s;
+
+                # and push an entity onto @parts
+                push @parts, MIME::Entity->build(
+                    Top          => 0,
+                    Data         => $canvas_image->png,
+                    Type         => 'image/png',
+                    Encoding     => 'base64',
+                    Disposition  => 'inline',
+                    'Content-Id' => "<$cid>",
+                );
+            }
+            }
+        }
+        else {
+            RT->Logger->warn('EmailDashboardJSChartImages is enabled but WWW::Mechanize::Chrome is not installed. Install the optional module 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 ( CSS::Inliner->require ) {
@@ -547,6 +654,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 469c9208ec..c394310295 100644
--- a/sbin/rt-email-dashboards.in
+++ b/sbin/rt-email-dashboards.in
@@ -90,7 +90,7 @@ RT::LoadConfig();
 RT->Config->Set( LogToSTDERR => $opts{log} ) if $opts{log};
 
 # Disable JS chart as email clients don't support it
-RT->Config->Set( EnableJSChart => 0 );
+RT->Config->Set( EnableJSChart => RT->Config->Get( 'EmailDashboardJSChartImages' ) );
 
 # Disable inline editing as email clients don't support it
 RT->Config->Set( InlineEdit => 0 );

-----------------------------------------------------------------------


hooks/post-receive
-- 
rt


More information about the rt-commit mailing list