[Rt-commit] rt branch 6.0/upgrade-ckeditor5 updated. rt-5.0.5-156-g4265a17b25
BPS Git Server
git at git.bestpractical.com
Tue Jan 9 21:56:37 UTC 2024
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, 6.0/upgrade-ckeditor5 has been updated
via 4265a17b25243baef8e0356cabec12ad1cf4f7d6 (commit)
via d93d47cf1bfe0f5009b83057499d75a4e7b7f176 (commit)
from ea997b3b8c0519d79832bae0770218702f9a5559 (commit)
Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.
- Log -----------------------------------------------------------------
commit 4265a17b25243baef8e0356cabec12ad1cf4f7d6
Author: sunnavy <sunnavy at bestpractical.com>
Date: Tue Jan 9 16:48:33 2024 -0500
Test extracting inline/internal images from HTML content
diff --git a/t/web/inline_images.t b/t/web/inline_images.t
new file mode 100644
index 0000000000..b749f23995
--- /dev/null
+++ b/t/web/inline_images.t
@@ -0,0 +1,125 @@
+use strict;
+use warnings;
+
+# Setting CorrespondAddress to make sure the From header of emails is set correctly
+# Otherwise email parser would warn something about:
+# "Enoch Root via RT" <> is not a valid email address and is not user name
+
+use RT::Test tests => undef, config => q{Set($CorrespondAddress, 'general at example.com');};
+use MIME::Base64;
+
+my ( $baseurl, $m ) = RT::Test->started_ok;
+
+ok $m->login;
+
+my $image_file = RT::Test::get_relocatable_file( 'bpslogo.png', '..', 'data' );
+my $image_content;
+{
+ local $/;
+ open my $fh, '<', $image_file;
+ $image_content = <$fh>;
+}
+
+my $content = q{Test inline images: <img src="data:image/png;base64,} . encode_base64($image_content) . q{">};
+
+diag "Testing inline images";
+{
+ $m->goto_create_ticket('General');
+ $m->submit_form(
+ form_name => 'TicketCreate',
+ fields => {
+ Subject => 'Test inline images',
+ Content => $content,
+ },
+ button => 'SubmitTicket'
+ );
+
+ my $ticket = RT::Test->last_ticket;
+ my $create_txn = $ticket->Transactions->First;
+ my $create_attachments = $create_txn->Attachments->ItemsArrayRef;
+ is( @$create_attachments, 3, 'Found 3 attachments in create transaction' );
+ is( $create_attachments->[0]->ContentType, 'multipart/related', 'First attachment is the multipart/related' );
+ is( $create_attachments->[1]->ContentType, 'text/html', 'Second attachment is the html' );
+ is( $create_attachments->[2]->ContentType, 'image/png', 'Third attachment is the image' );
+ is( $create_attachments->[2]->Content, $image_content, 'Image attachment content is correct' );
+
+ my $dom = $m->dom;
+ my $image_url = 'Attachment/' . $create_txn->Id . '/' . $create_attachments->[2]->Id;
+ ok( $dom->at(qq{img[src="$image_url"]}), 'Image displayed inline' );
+ $m->text_contains('Image displayed inline above');
+
+ my @mails = RT::Test->fetch_caught_mails;
+ is( @mails, 1, 'Got 1 email' );
+ my $entity = parse_mail( $mails[0] );
+ is( $entity->mime_type, 'multipart/alternative', 'Email is multipart/alternative' );
+
+ my @parts = $entity->parts;
+ is( @parts, 2, 'Got 2 parts' );
+ is( $parts[0]->mime_type, 'text/plain', 'First part is text/plain' );
+ is( $parts[1]->mime_type, 'multipart/related', 'Second part is multipart/related' );
+
+ @parts = $parts[1]->parts;
+ is( @parts, 2, 'Got 2 parts in multipart/related' );
+ is( $parts[0]->mime_type, 'text/html', 'First part is text/html' );
+ is( $parts[1]->mime_type, 'image/png', 'Second part is image/png' );
+ my ($cid) = $parts[1]->head->get('Content-ID') =~ /<(.+)>/;
+ like( $parts[0]->body_as_string, qr/img src="cid:\Q$cid\E"/, 'HTML content contains correct image src' );
+ is( $parts[1]->bodyhandle->as_string, $image_content, 'Image content is correct' );
+}
+
+diag "Testing quoted images";
+{
+
+ my $dom = $m->dom;
+ my $img = $dom->at(qq{img[src^="Attachment/"]});
+
+ $m->follow_link_ok( { url_regex => qr/QuoteTransaction=/ }, 'Reply the create transaction' );
+ $dom = $m->dom;
+ my $textarea = $dom->at('textarea');
+ like( $textarea->content, qr!<img src="/Ticket/@{[$img->attr('src')]}!,
+ 'Quoted html contains converted image URL' );
+
+ $m->submit_form(
+ form_name => 'TicketUpdate',
+ fields => {
+ UpdateCc => 'test at example.com',
+ },
+ button => 'SubmitTicket'
+ );
+
+ my $ticket = RT::Test->last_ticket;
+ my $txns = $ticket->Transactions;
+ $txns->Limit( FIELD => 'Type', VALUE => 'Correspond' );
+ my $correspond_txn = $txns->First;
+ my $correspond_attachments = $correspond_txn->Attachments->ItemsArrayRef;
+ is( @$correspond_attachments, 3, 'Found 3 attachments in correspond transaction' );
+ is( $correspond_attachments->[0]->ContentType, 'multipart/related', 'First attachment is the multipart/related' );
+ is( $correspond_attachments->[1]->ContentType, 'text/html', 'Second attachment is the html' );
+ is( $correspond_attachments->[2]->ContentType, 'image/png', 'Third attachment is the image' );
+ is( $correspond_attachments->[2]->Content, $image_content, 'Image attachment content is correct' );
+ my $image_url = 'Attachment/' . $correspond_txn->Id . '/' . $correspond_attachments->[2]->Id;
+
+ $dom = $m->dom;
+ ok( $dom->at(qq{img[src="$image_url"]}), 'Image displayed inline' );
+ $m->text_like( qr/(Image displayed inline above).*\1/s, 'Image displayed inline above' );
+
+ my @mails = RT::Test->fetch_caught_mails;
+ is( @mails, 1, 'Got 1 email' );
+ my $entity = parse_mail( $mails[0] );
+ is( $entity->mime_type, 'multipart/alternative', 'Email is multipart/alternative' );
+
+ my @parts = $entity->parts;
+ is( @parts, 2, 'Got 2 parts' );
+ is( $parts[0]->mime_type, 'text/plain', 'First part is text/plain' );
+ is( $parts[1]->mime_type, 'multipart/related', 'Second part is multipart/related' );
+
+ @parts = $parts[1]->parts;
+ is( @parts, 2, 'Got 2 parts in multipart/related' );
+ is( $parts[0]->mime_type, 'text/html', 'First part is text/html' );
+ is( $parts[1]->mime_type, 'image/png', 'Second part is image/png' );
+ my ($cid) = $parts[1]->head->get('Content-ID') =~ /<(.+)>/;
+ like( $parts[0]->body_as_string, qr/img src="cid:\Q$cid\E"/, 'HTML content contains correct image src' );
+ is( $parts[1]->bodyhandle->as_string, $image_content, 'Image content is correct' );
+}
+
+done_testing();
commit d93d47cf1bfe0f5009b83057499d75a4e7b7f176
Author: sunnavy <sunnavy at bestpractical.com>
Date: Mon Jan 8 16:47:31 2024 -0500
Extract inline/internal images from HTML content
Previously inline images were embedded in HTML, which was not quite
efficient. This commit extracts them to individual image parts, not only
more efficient but also makes things easier to link to these images on
correspond/comment.
For new transactions with images linking to existing attachments(usually
via quoted transactions), we also extract these linked images like inline
images, so new created transactions could be independent from quoted
transactions.
For outgoing emails that contain inline images, this commit automatically
converts them to corresponding "multipart/related" entities that contain
both HTML and linked images, which is more consistent with email RFCs.
diff --git a/lib/RT/Action/SendEmail.pm b/lib/RT/Action/SendEmail.pm
index 02df3d94f9..36bfee148e 100644
--- a/lib/RT/Action/SendEmail.pm
+++ b/lib/RT/Action/SendEmail.pm
@@ -182,9 +182,10 @@ sub Prepare {
&& !$MIMEObj->head->get('To')
&& ( $MIMEObj->head->get('Cc') or $MIMEObj->head->get('Bcc') );
- # For security reasons, we only send out textual mails.
+ # For security reasons, we only send out textual+image mails.
foreach my $part ( grep !$_->is_multipart, $MIMEObj->parts_DFS ) {
my $type = $part->mime_type || 'text/plain';
+ next if $type =~ m{^image/};
$type = 'text/plain' unless RT::I18N::IsTextualContentType($type);
$part->head->mime_attr( "Content-Type" => $type );
# utf-8 here is for _FindOrGuessCharset in I18N.pm
@@ -396,6 +397,8 @@ sub AddAttachments {
# attach any of this transaction's attachments
my $seen_attachment = 0;
while ( my $attach = $attachments->Next ) {
+ # Skip if it's already added(as inline) in template.
+ next if $self->TemplateObj->{_AddedAttachments} && $self->TemplateObj->{_AddedAttachments}{ $attach->Id };
if ( !$seen_attachment ) {
$MIMEObj->make_multipart( 'mixed', Force => 1 );
$seen_attachment = 1;
diff --git a/lib/RT/Interface/Web.pm b/lib/RT/Interface/Web.pm
index abc91701ac..0f4d082329 100644
--- a/lib/RT/Interface/Web.pm
+++ b/lib/RT/Interface/Web.pm
@@ -81,6 +81,8 @@ use HTTP::Status qw();
use Regexp::Common;
use RT::Shortener;
use RT::Interface::Web::ReportsRegistry;
+use MIME::Base64;
+use Digest::SHA 'sha1_hex';
our @SHORTENER_SEARCH_FIELDS
= qw/Class ObjectType BaseQuery Query Format RowsPerPage Order OrderBy ExtraQueryParams ResultPage/;
@@ -2182,6 +2184,68 @@ sub ExpandShortenerCode {
}
}
+=head2 ExtractImages Content => $Content, CurrentUser => $CurrentUser
+
+Extract images from $HTML and convert them to src="cid:..."
+
+Currently it supports images embedded in base64 and ones linking to existing
+ticket attachments.
+
+Returns the modified HTML and extracted images, each image is a hashref
+containing:
+
+ cid: content id
+ content_type: image type
+ content: image data
+
+=cut
+
+sub ExtractImages {
+ my %args = (
+ Content => undef,
+ CurrentUser => $HTML::Mason::Commands::session{CurrentUser},
+ @_,
+ );
+
+ my $content = $args{Content};
+ my ( @images, %added );
+ require HTML::RewriteAttributes::Resources;
+ $content = HTML::RewriteAttributes::Resources->rewrite(
+ $content,
+ sub {
+ my $uri = shift;
+ my %meta = @_;
+ return $uri unless lc $meta{tag} eq 'img' && lc $meta{attr} eq 'src';
+
+ my ( $content_type, $content );
+ if ( $uri =~ m{^data:(.+);base64,(.+)}s ) {
+ $content_type = $1;
+ $content = decode_base64($2);
+ }
+ elsif ( $uri =~ m{^/(?:SelfService|Ticket)/Attachment/\d+/(\d+)} ) {
+ my $attachment = RT::Attachment->new( $args{CurrentUser} );
+ $attachment->Load($1);
+ if ( $attachment->CurrentUserCanSee ) {
+ $content_type = $attachment->ContentType;
+ $content = $attachment->Content;
+ }
+ else {
+ RT->Logger->warning( "Attachment #$1 is not visible to current user #" . $args{CurrentUser}->Id );
+ }
+ }
+
+ if ($content) {
+ my $cid = sha1_hex($content) . '@' . RT->Config->Get('rtname');
+ push @images, { cid => $cid, content => $content, content_type => $content_type } unless $added{$cid}++;
+ return "cid:$cid";
+ }
+
+ return $uri;
+ }
+ );
+ return ( $content, @images );
+}
+
package HTML::Mason::Commands;
use vars qw/$r $m %session/;
@@ -2529,7 +2593,7 @@ sub CreateTicket {
push @attachments, grep $_, map $ARGS{Attachments}->{$_}, sort keys %{ $ARGS{'Attachments'} };
}
if ( @attachments ) {
- $MIMEObj->make_multipart;
+ $MIMEObj->make_multipart( 'mixed', Force => 1 );
$MIMEObj->add_part( $_ ) foreach @attachments;
}
@@ -2720,7 +2784,7 @@ sub ProcessUpdateMessage {
}
if ( @attachments ) {
- $Message->make_multipart;
+ $Message->make_multipart( 'mixed', Force => 1 );
$Message->add_part( $_ ) foreach @attachments;
}
@@ -2885,7 +2949,11 @@ sub ProcessAttachments {
Takes a paramhash Subject, Body and AttachmentFieldName.
-Also takes Form, Cc and Type as optional paramhash keys.
+Also takes Form, Cc, Type, and ExtractImages as optional paramhash keys.
+
+If ExtractImages is true(default value), it will extract images from the HTML
+body and generate a corresponding "multiplart/related" entity that contains
+the modified body and also extracted images.
Returns a MIME::Entity.
@@ -2902,8 +2970,15 @@ sub MakeMIMEEntity {
AttachmentFieldName => undef,
Type => undef,
Interface => undef,
+ ExtractImages => 1,
@_,
);
+
+ my @images;
+ if ( $args{ExtractImages} && ( $args{Type} // '' ) eq 'text/html' ) {
+ ( $args{Body}, @images ) = RT::Interface::Web::ExtractImages( Content => $args{Body} );
+ }
+
my $Message = MIME::Entity->build(
Type => 'multipart/mixed',
"Message-Id" => Encode::encode( "UTF-8", RT::Interface::Email::GenMessageId ),
@@ -2959,6 +3034,20 @@ sub MakeMIMEEntity {
RT::I18N::SetMIMEEntityToUTF8($Message); # convert text parts into utf-8
+ if (@images) {
+ $Message->make_multipart('related');
+ # RFC2387 3.1 says that "type" must be specified
+ $Message->head->mime_attr('Content-type.type' => 'text/html');
+ for my $image (@images) {
+ $Message->attach(
+ Type => $image->{content_type},
+ Data => $image->{content},
+ Disposition => 'inline',
+ Id => $image->{cid},
+ );
+ }
+ }
+
return ($Message);
}
diff --git a/lib/RT/Template.pm b/lib/RT/Template.pm
index 87ea9940a8..6fa6b0f277 100644
--- a/lib/RT/Template.pm
+++ b/lib/RT/Template.pm
@@ -406,6 +406,7 @@ sub Parse {
my $self = shift;
my ($rv, $msg);
+ delete $self->{_AddedAttachments};
if (not $self->IsEmpty and $self->Content =~ m{^Content-Type:\s+text/html\b}im) {
local $RT::Transaction::PreferredContentType = 'text/html';
@@ -417,8 +418,47 @@ sub Parse {
return ($rv, $msg) unless $rv;
+ my %args = @_;
my $mime_type = $self->MIMEObj->mime_type;
- if (defined $mime_type and $mime_type eq 'text/html') {
+ if ( defined $mime_type and $mime_type eq 'text/html' and $args{TransactionObj} ) {
+ if ( my $content_obj = $args{TransactionObj}->ContentObj( Type => 'text/html' ) ) {
+ if ( my $related_part = $content_obj->Closest("multipart/related") ) {
+ my $body = Encode::decode( "UTF-8", $self->MIMEObj->bodyhandle->as_string );
+ my ( @attachments, %added );
+ require HTML::RewriteAttributes::Resources;
+ HTML::RewriteAttributes::Resources->rewrite(
+ $body,
+ sub {
+ my $cid = shift;
+ my %meta = @_;
+ return $cid unless lc $meta{tag} eq 'img' && lc $meta{attr} eq 'src' && $cid =~ s/^cid://i;
+
+ for my $attach ( @{$related_part->Children->ItemsArrayRef } ) {
+ if ( ( $attach->GetHeader('Content-ID') || '' ) =~ /^(<)?\Q$cid\E(?(1)>)$/ ) {
+ push @attachments, $attach unless $added{$attach->Id}++;
+ }
+ }
+
+ return "cid:$cid";
+ }
+ );
+
+ if ( @attachments ) {
+ $self->MIMEObj->make_multipart('related');
+ # RFC2387 3.1 says that "type" must be specified
+ $self->MIMEObj->head->mime_attr('Content-type.type' => 'text/html');
+ for my $attach ( @attachments ) {
+ $self->MIMEObj->attach(
+ Type => $attach->ContentType,
+ Disposition => $attach->GetHeader('Content-Disposition'),
+ Id => $attach->GetHeader('Content-ID'),
+ Data => $attach->OriginalContent,
+ );
+ }
+ $self->{_AddedAttachments} = { map { $_->Id => 1 } @attachments };
+ }
+ }
+ }
$self->_DowngradeFromHTML(@_);
}
@@ -675,7 +715,8 @@ sub _DowngradeFromHTML {
my $self = shift;
my $orig_entity = $self->MIMEObj;
- my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
+ my $html_entity = $orig_entity->is_multipart ? $orig_entity->parts(0) : $orig_entity;
+ my $new_entity = $html_entity->dup; # this will fail badly if we go away from InCore parsing
# We're going to make this multipart/alternative below, so clear out the Subject
# header copied from the original when we dup'd above.
@@ -689,8 +730,8 @@ sub _DowngradeFromHTML {
$new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
$new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
- $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
- $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
+ $html_entity->head->mime_attr( "Content-Type" => 'text/html' );
+ $html_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
my $body = $new_entity->bodyhandle->as_string;
$body = Encode::decode( "UTF-8", $body );
diff --git a/lib/RT/Transaction.pm b/lib/RT/Transaction.pm
index 20283f28a0..43962c823d 100644
--- a/lib/RT/Transaction.pm
+++ b/lib/RT/Transaction.pm
@@ -711,6 +711,14 @@ sub _FindPreferredContentObj {
}
}
+
+ # Handle the case where multipart/related is a child of multipart/alternative.
+ my $related_parts = $Attachment->Children;
+ $related_parts->ContentType( VALUE => 'multipart/related' );
+ while ( my $child = $related_parts->Next ) {
+ my $ret = _FindPreferredContentObj( %args, Attachment => $child );
+ return $ret if $ret;
+ }
}
# If this is a message/rfc822 mail, we need to dig into it in order to find
diff --git a/share/html/Elements/MessageBox b/share/html/Elements/MessageBox
index 09d534d785..8cea5574a2 100644
--- a/share/html/Elements/MessageBox
+++ b/share/html/Elements/MessageBox
@@ -83,6 +83,16 @@ if ( $QuoteTransaction ) {
if ( $transaction->Id && !$QuoteContent ) {
$message = $transaction->Content( Quote => 1, Type => $Type );
+ # Convert cid: images to links so they can be rendered.
+ if ( $Type eq 'text/html' && $message && $transaction->ObjectType eq 'RT::Ticket' ) {
+ RT::Interface::Web::RewriteInlineImages(
+ Content => \$message,
+ Attachment => $transaction->ContentObj( Type => $Type ) || undef,
+ AttachmentPath => join( '/',
+ RT->Config->Get('WebPath'), $session{CurrentUser}->Privileged ? 'Ticket' : 'SelfService',
+ 'Attachment' ),
+ );
+ }
}
else {
$message = RT::Transaction->QuoteContent(
-----------------------------------------------------------------------
Summary of changes:
lib/RT/Action/SendEmail.pm | 5 +-
lib/RT/Interface/Web.pm | 95 ++++++++++++++++++++++++++++++-
lib/RT/Template.pm | 49 ++++++++++++++--
lib/RT/Transaction.pm | 8 +++
share/html/Elements/MessageBox | 10 ++++
t/web/inline_images.t | 125 +++++++++++++++++++++++++++++++++++++++++
6 files changed, 284 insertions(+), 8 deletions(-)
create mode 100644 t/web/inline_images.t
hooks/post-receive
--
rt
More information about the rt-commit
mailing list