rt 4.2.14 (#13852)
[freeside.git] / rt / lib / RT / Dashboard / Mailer.pm
index 40b53b1..0125dda 100644 (file)
@@ -2,7 +2,7 @@
 #
 # COPYRIGHT:
 #
-# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC
+# This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
 #                                          <sales@bestpractical.com>
 #
 # (Except where explicitly superseded by other copyright notices)
@@ -59,6 +59,8 @@ use RT::Dashboard;
 use RT::Interface::Web::Handler;
 use RT::Interface::Web;
 use File::Temp 'tempdir';
+use HTML::Scrubber;
+use URI::QueryParam;
 
 sub MailDashboards {
     my $self = shift;
@@ -145,7 +147,7 @@ sub IsSubscriptionReady {
     my $sub_hour      = $subscription->SubValue('Hour');
     my $sub_dow       = $subscription->SubValue('Dow');
     my $sub_dom       = $subscription->SubValue('Dom');
-    my $sub_fow       = $subscription->SubValue('Fow');
+    my $sub_fow       = $subscription->SubValue('Fow') || 1;
 
     my ($hour, $dow, $dom) = @{ $args{LocalTime} };
 
@@ -164,8 +166,6 @@ sub IsSubscriptionReady {
         return 0 if $sub_dow ne $dow;
 
         # does it match the "every N weeks" clause?
-        $sub_fow = 1 if !$sub_fow;
-
         return 1 if $counter % $sub_fow == 0;
 
         $subscription->SetSubValues(Counter => $counter + 1)
@@ -248,6 +248,8 @@ SUMMARY
         }
     }
 
+    $content = ScrubContent($content);
+
     $RT::Logger->debug("Got ".length($content)." characters of output.");
 
     $content = HTML::RewriteAttributes::Links->rewrite(
@@ -348,6 +350,7 @@ sub EmailDashboard {
     $RT::Logger->debug('Mailing dashboard "'.$dashboard->Name.'" to user '.$currentuser->Name." <$email>");
 
     my $ok = RT::Interface::Email::SendEmail(
+        %{ RT->Config->Get('Crypt')->{'Dashboards'} || {} },
         Entity => $entity,
     );
 
@@ -378,12 +381,19 @@ sub BuildEmail {
             # already attached this object
             return "cid:$cid_of{$uri}" if $cid_of{$uri};
 
-            $cid_of{$uri} = time() . $$ . int(rand(1e6));
             my ($data, $filename, $mimetype, $encoding) = GetResource($uri);
+            return $uri unless defined $data;
 
-            # downgrade non-text strings, because all strings are utf8 by
-            # default, which is wrong for non-text strings.
-            if ( $mimetype !~ m{text/} ) {
+            $cid_of{$uri} = time() . $$ . int(rand(1e6));
+
+            # Encode textual data in UTF-8, and downgrade (treat
+            # codepoints as codepoints, and ensure the UTF-8 flag is
+            # off) everything else.
+            my @extra;
+            if ( $mimetype =~ m{text/} ) {
+                $data = Encode::encode( "UTF-8", $data );
+                @extra = ( Charset => "UTF-8" );
+            } else {
                 utf8::downgrade( $data, 1 ) or $RT::Logger->warning("downgrade $data failed");
             }
 
@@ -393,8 +403,9 @@ sub BuildEmail {
                 Type         => $mimetype,
                 Encoding     => $encoding,
                 Disposition  => 'inline',
-                Name         => $filename,
+                Name         => RT::Interface::Email::EncodeToMIME( String => $filename ),
                 'Content-Id' => $cid_of{$uri},
+                @extra,
             );
 
             return "cid:$cid_of{$uri}";
@@ -402,29 +413,32 @@ sub BuildEmail {
         inline_css => sub {
             my $uri = shift;
             my ($content) = GetResource($uri);
-            return $content;
+            return defined $content ? $content : "";
         },
         inline_imports => 1,
     );
 
     my $entity = MIME::Entity->build(
-        From    => $args{From},
-        To      => $args{To},
-        Subject => $args{Subject},
+        From    => Encode::encode("UTF-8", $args{From}),
+        To      => Encode::encode("UTF-8", $args{To}),
+        Subject => RT::Interface::Email::EncodeToMIME( String => $args{Subject} ),
         Type    => "multipart/mixed",
     );
 
     $entity->attach(
-        Data        => Encode::encode_utf8($content),
         Type        => 'text/html',
         Charset     => 'UTF-8',
+        Data        => Encode::encode("UTF-8", $content),
         Disposition => 'inline',
+        Encoding    => "base64",
     );
 
     for my $part (@parts) {
         $entity->add_part($part);
     }
 
+    $entity->make_singlepart;
+
     return $entity;
 }
 
@@ -447,7 +461,7 @@ sub BuildEmail {
                 autohandler_name => '', # disable forced login and more
                 data_dir => $data_dir,
             );
-            $mason->set_escape( h => \&RT::Interface::Web::EscapeUTF8 );
+            $mason->set_escape( h => \&RT::Interface::Web::EscapeHTML );
             $mason->set_escape( u => \&RT::Interface::Web::EscapeURI  );
             $mason->set_escape( j => \&RT::Interface::Web::EscapeJS   );
         }
@@ -463,6 +477,33 @@ sub BuildEmail {
 }
 
 {
+    my $scrubber;
+
+    sub _scrubber {
+        unless ($scrubber) {
+            $scrubber = HTML::Scrubber->new;
+            # Allow everything by default, except JS attributes ...
+            $scrubber->default(
+                1 => {
+                    '*' => 1,
+                    map { ("on$_" => 0) }
+                         qw(blur change click dblclick error focus keydown keypress keyup load
+                            mousedown mousemove mouseout mouseover mouseup reset select submit unload)
+                }
+            );
+            # ... and <script>s
+            $scrubber->deny('script');
+        }
+        return $scrubber;
+    }
+
+    sub ScrubContent {
+        my $content = shift;
+        return _scrubber->scrub($content);
+    }
+}
+
+{
     my %cache;
 
     sub HourDowDomIn {
@@ -493,7 +534,13 @@ sub BuildEmail {
 
 sub GetResource {
     my $uri = URI->new(shift);
-    my ($content, $filename, $mimetype, $encoding);
+    my ($content, $content_type, $filename, $mimetype, $encoding);
+
+    # Avoid trying to inline any remote URIs.  We absolutified all URIs
+    # using WebURL in SendDashboard() above, so choose the simpler match on
+    # that rather than testing a bunch of URI accessors.
+    my $WebURL = RT->Config->Get("WebURL");
+    return unless $uri =~ /^\Q$WebURL/;
 
     $RT::Logger->debug("Getting resource $uri");
 
@@ -506,40 +553,35 @@ sub GetResource {
     $path = "/$path"
         unless $path =~ m{^/};
 
-    $HTML::Mason::Commands::r->path_info($path);
-
-    # grab the query arguments
-    my %args;
-    for (split /&/, ($uri->query||'')) {
-        my ($k, $v) = /^(.*?)=(.*)$/
-            or die "Unable to parse query parameter '$_'";
-
-        for ($k, $v) { s/%(..)/chr hex $1/ge }
-
-        # no value yet, simple key=value
-        if (!exists $args{$k}) {
-            $args{$k} = $v;
-        }
-        # already have key=value, need to upgrade it to key=[value1, value2]
-        elsif (!ref($args{$k})) {
-            $args{$k} = [$args{$k}, $v];
-        }
-        # already key=[value1, value2], just add the new value
-        else {
-            push @{ $args{$k} }, $v;
-        }
+    # Try the static handler first for non-Mason CSS, JS, etc.
+    my $res = RT::Interface::Web::Handler->GetStatic($path);
+    if ($res->is_success) {
+        RT->Logger->debug("Fetched '$path' from the static handler");
+        $content      = $res->decoded_content;
+        $content_type = $res->headers->content_type;
+    } else {
+        # Try it through Mason instead...
+        $HTML::Mason::Commands::r->path_info($path);
+
+        # grab the query arguments
+        my %args = map { $_ => [ map {Encode::decode("UTF-8",$_)}
+                                     $uri->query_param($_) ] } $uri->query_param;
+        # Convert empty and single element arrayrefs to a non-ref scalar
+        @$_ < 2 and $_ = $_->[0]
+            for values %args;
+
+        $RT::Logger->debug("Running component '$path'");
+        $content = RunComponent($path, %args);
+
+        $content_type = $HTML::Mason::Commands::r->content_type;
     }
 
-    $RT::Logger->debug("Running component '$path'");
-    $content = RunComponent($path, %args);
-
     # guess at the filename from the component name
     $filename = $1 if $path =~ m{^.*/(.*?)$};
 
     # the rest of this was taken from Email::MIME::CreateHTML::Resolver::LWP
     ($mimetype, $encoding) = MIME::Types::by_suffix($filename);
 
-    my $content_type = $HTML::Mason::Commands::r->content_type;
     if ($content_type) {
         $mimetype = $content_type;
 
@@ -560,8 +602,9 @@ sub GetResource {
 {
     package RT::Dashboard::FakeRequest;
     sub new { bless {}, shift }
-    sub header_out { shift }
-    sub headers_out { shift }
+    sub header_out { return undef }
+    sub headers_out { wantarray ? () : {} }
+    sub err_headers_out { wantarray ? () : {} }
     sub content_type {
         my $self = shift;
         $self->{content_type} = shift if @_;