Fixed #2399 - Attached images are broken on trigger reply with #{article.body_as_html}

This commit is contained in:
Billy Zhou 2019-02-10 09:40:55 +01:00 committed by Martin Edenhofer
parent f7a59c6bbd
commit 487f36a5a5
6 changed files with 264 additions and 35 deletions

View file

@ -6,41 +6,7 @@ module ClonesTicketArticleAttachments
def article_attachments_clone(article) def article_attachments_clone(article)
raise Exceptions::UnprocessableEntity, 'Need form_id to attach attachments to new form.' if params[:form_id].blank? raise Exceptions::UnprocessableEntity, 'Need form_id to attach attachments to new form.' if params[:form_id].blank?
existing_attachments = Store.list( article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments: true)
object: 'UploadCache',
o_id: params[:form_id],
)
attachments = []
article.attachments.each do |new_attachment|
next if new_attachment.preferences['content-alternative'] == true
if article.content_type.present? && article.content_type =~ %r{text/html}i
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] !~ /inline/
if new_attachment.preferences['Content-ID'].present? && article.body.present?
next if article.body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
end
end
already_added = false
existing_attachments.each do |existing_attachment|
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
already_added = true
break
end
next if already_added == true
file = Store.add(
object: 'UploadCache',
o_id: params[:form_id],
data: new_attachment.content,
filename: new_attachment.filename,
preferences: new_attachment.preferences,
)
attachments.push file
end
attachments
end end
end end

View file

@ -1486,6 +1486,12 @@ result
preferences: attachment[:preferences], preferences: attachment[:preferences],
) )
end end
original_article = objects[:article]
if original_article&.should_clone_inline_attachments? # rubocop:disable Style/GuardClause
original_article.clone_attachments('Ticket::Article', message.id, only_inline_attachments: true)
original_article.should_clone_inline_attachments = false # cancel the temporary flag after cloning
end
end end
def sms_recipients_by_type(recipient_type, article) def sms_recipients_by_type(recipient_type, article)

View file

@ -44,6 +44,9 @@ class Ticket::Article < ApplicationModel
:to, :to,
:cc :cc
attr_accessor :should_clone_inline_attachments
alias should_clone_inline_attachments? should_clone_inline_attachments
# fillup md5 of message id to search easier on very long message ids # fillup md5 of message id to search easier on very long message ids
def check_message_id_md5 def check_message_id_md5
return true if message_id.blank? return true if message_id.blank?
@ -130,6 +133,77 @@ returns
new_attachments new_attachments
end end
=begin
clone existing attachments of article to the target object
article_parent = Ticket::Article.find(123)
article_new = Ticket::Article.find(456)
attached_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
inline_attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true)
returns
[attachment1, attachment2, ...]
=end
def clone_attachments(object_type, object_id, options = {})
existing_attachments = Store.list(
object: object_type,
o_id: object_id,
)
is_html_content = false
if content_type.present? && content_type =~ %r{text/html}i
is_html_content = true
end
new_attachments = []
attachments.each do |new_attachment|
next if new_attachment.preferences['content-alternative'] == true
# only_attached_attachments mode is used by apply attached attachments to forwared article
if options[:only_attached_attachments] == true
if is_html_content
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] =~ /inline/
next if new_attachment.preferences['Content-ID'].blank?
next if body.present? && body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
end
end
# only_inline_attachments mode is used when quoting HTML mail with #{article.body_as_html}
if options[:only_inline_attachments] == true
next if is_html_content == false
next if body.blank?
next if new_attachment.preferences['content_disposition'].present? && new_attachment.preferences['content_disposition'] !~ /inline/
next if new_attachment.preferences['Content-ID'].present? && !body.match?(/#{Regexp.quote(new_attachment.preferences['Content-ID'])}/i)
end
already_added = false
existing_attachments.each do |existing_attachment|
next if existing_attachment.filename != new_attachment.filename || existing_attachment.size != new_attachment.size
already_added = true
break
end
next if already_added == true
file = Store.add(
object: object_type,
o_id: object_id,
data: new_attachment.content,
filename: new_attachment.filename,
preferences: new_attachment.preferences,
)
new_attachments.push file
end
new_attachments
end
def self.last_customer_agent_article(ticket_id) def self.last_customer_agent_article(ticket_id)
sender = Ticket::Article::Sender.lookup(name: 'System') sender = Ticket::Article::Sender.lookup(name: 'System')
Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order('created_at DESC').first Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order('created_at DESC').first

View file

@ -126,6 +126,11 @@ examples how to use
begin begin
previous_object_refs = object_refs previous_object_refs = object_refs
object_refs = object_refs.send(method.to_sym, *arguments) object_refs = object_refs.send(method.to_sym, *arguments)
# body_as_html should trigger the cloning of all inline attachments from the parent article (issue #2399)
if method.to_sym == :body_as_html && previous_object_refs.respond_to?(:should_clone_inline_attachments)
previous_object_refs.should_clone_inline_attachments = true
end
rescue => e rescue => e
value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}" value = "\#{#{object_name}.#{object_methods_s} / #{e.message}}"
break break

View file

@ -124,4 +124,95 @@ RSpec.describe Ticket::Article, type: :model do
end end
end end
end end
describe 'clone attachments' do
context 'of forwarded article' do
context 'via email' do
it 'only need to clone attached attachments' do
article_parent = create(:ticket_article,
type: Ticket::Article::Type.find_by(name: 'email'),
content_type: 'text/html',
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file1_normally_should_be_an_image',
filename: 'some_file1.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file2_normally_should_be_an_image',
filename: 'some_file2.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
article_new = create(:ticket_article)
UserInfo.current_user_id = 1
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_attached_attachments: true)
expect(attachments.count).to eq(1)
expect(attachments[0].filename).to eq('some_file2.jpg')
end
end
end
context 'of trigger' do
context 'via email notifications' do
it 'only need to clone inline attachments used in body' do
article_parent = create(:ticket_article,
type: Ticket::Article::Type.find_by(name: 'email'),
content_type: 'text/html',
body: '<img src="cid:15.274327094.140938@zammad.example.com"> some text',)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file1_normally_should_be_an_image',
filename: 'some_file1.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
Store.add(
object: 'Ticket::Article',
o_id: article_parent.id,
data: 'content_file2_normally_should_be_an_image',
filename: 'some_file2.jpg',
preferences: {
'Content-Type' => 'image/jpeg',
'Mime-Type' => 'image/jpeg',
'Content-ID' => '15.274327094.140938_not_reffered@zammad.example.com',
'Content-Disposition' => 'inline',
},
created_by_id: 1,
)
article_new = create(:ticket_article)
UserInfo.current_user_id = 1
attachments = article_parent.clone_attachments(article_new.class.name, article_new.id, only_inline_attachments: true )
expect(attachments.count).to eq(1)
expect(attachments[0].filename).to eq('some_file1.jpg')
end
end
end
end
end end

View file

@ -4596,4 +4596,91 @@ class TicketTriggerTest < ActiveSupport::TestCase
end end
end end
#2399 - Attached images are broken on trigger reply with #{article.body_as_html}
test 'make sure auto reply using #{article.body_as_html} copies all articles image attachments as well' do
# make sure that this auto reply trigger only reacts to this particular test in order not to interfer with other auto reply tests
trigger1 = Trigger.create!(
name: 'auto reply with HTML quote',
condition: {
'ticket.action' => {
'operator' => 'is',
'value' => 'create',
},
'ticket.state_id' => {
'operator' => 'is',
'value' => Ticket::State.lookup(name: 'new').id.to_s,
},
'ticket.title' => {
'operator' => 'contains',
'value' => 'AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545]',
},
},
perform: {
'notification.email' => {
'body' => '#{article.body_as_html}',
'recipient' => 'article_last_sender',
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
ticket1, article1, user, mail = Channel::EmailParser.new.process({}, File.read(Rails.root.join('test', 'data', 'mail', 'mail048.box')))
assert_equal('AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545]', ticket1.title, 'ticket1.title verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(2, ticket1.articles.first.attachments.count)
article1 = ticket1.articles.last
assert_match('Thanks for your inquiry (AW: OTRS / Anfrage OTRS Einführung/Präsentation [Ticket#11545])!', article1.subject)
assert_equal(1, article1.attachments.count)
assert_equal('50606', article1.attachments[0].size)
assert_equal('CPG-Reklamationsmitteilung bezügl.01234567895 an Voda-28.03.2017.jpg', article1.attachments[0].filename)
end
#2399 - Attached images are broken on trigger reply with #{article.body_as_html}
test 'make sure auto reply using #{article.body_as_html} does not copy any non-image attachments' do
# make sure that this auto reply trigger only reacts to this particular test in order not to interfer with other auto reply tests
trigger1 = Trigger.create!(
name: 'auto reply with HTML quote',
condition: {
'ticket.action' => {
'operator' => 'is',
'value' => 'create',
},
'ticket.state_id' => {
'operator' => 'is',
'value' => Ticket::State.lookup(name: 'new').id.to_s,
},
'ticket.title' => {
'operator' => 'contains',
'value' => 'Online-apotheke. Günstigster Preis. Ohne Rezepte',
},
},
perform: {
'notification.email' => {
'body' => '#{article.body_as_html}',
'recipient' => 'article_last_sender',
'subject' => 'Thanks for your inquiry (#{ticket.title})!',
},
},
disable_notification: true,
active: true,
created_by_id: 1,
updated_by_id: 1,
)
ticket1, article1, user, mail = Channel::EmailParser.new.process({}, File.read(Rails.root.join('test', 'data', 'mail', 'mail069.box')))
assert_equal('Online-apotheke. Günstigster Preis. Ohne Rezepte', ticket1.title, 'ticket1.title verify')
assert_equal(2, ticket1.articles.count, 'ticket1.articles verify')
assert_equal(1, ticket1.articles.first.attachments.count)
article1 = ticket1.articles.last
assert_match('Thanks for your inquiry (Online-apotheke. Günstigster Preis. Ohne Rezepte)!', article1.subject)
assert_equal(0, article1.attachments.count)
end
end end