2016-10-19 03:11:36 +00:00
|
|
|
# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
|
2015-04-27 23:19:26 +00:00
|
|
|
class Ticket::Article < ApplicationModel
|
2017-05-02 15:21:13 +00:00
|
|
|
include HasActivityStreamLog
|
|
|
|
include ChecksClientNotification
|
|
|
|
include HasHistory
|
|
|
|
include ChecksHtmlSanitized
|
2017-06-16 20:43:09 +00:00
|
|
|
include Ticket::Article::ChecksAccess
|
2017-01-31 17:13:45 +00:00
|
|
|
|
2015-04-27 23:19:26 +00:00
|
|
|
load 'ticket/article/assets.rb'
|
|
|
|
include Ticket::Article::Assets
|
|
|
|
|
|
|
|
belongs_to :ticket
|
2017-04-07 14:44:34 +00:00
|
|
|
has_one :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', foreign_key: :ticket_article_id, dependent: :destroy
|
2015-04-27 23:19:26 +00:00
|
|
|
belongs_to :type, class_name: 'Ticket::Article::Type'
|
|
|
|
belongs_to :sender, class_name: 'Ticket::Article::Sender'
|
|
|
|
belongs_to :created_by, class_name: 'User'
|
|
|
|
belongs_to :updated_by, class_name: 'User'
|
2015-06-05 14:19:36 +00:00
|
|
|
store :preferences
|
2017-09-08 07:24:07 +00:00
|
|
|
before_create :check_subject, :check_body, :check_message_id_md5
|
|
|
|
before_update :check_subject, :check_body, :check_message_id_md5
|
2015-08-30 18:16:29 +00:00
|
|
|
|
2017-02-01 11:48:50 +00:00
|
|
|
sanitized_html :body
|
|
|
|
|
2017-01-31 17:13:45 +00:00
|
|
|
activity_stream_permission 'ticket.agent'
|
2015-04-27 23:19:26 +00:00
|
|
|
|
2017-01-31 17:13:45 +00:00
|
|
|
activity_stream_attributes_ignored :type_id,
|
|
|
|
:sender_id,
|
|
|
|
:preferences
|
2016-08-12 16:39:09 +00:00
|
|
|
|
2017-01-31 17:13:45 +00:00
|
|
|
history_attributes_ignored :type_id,
|
|
|
|
:sender_id,
|
|
|
|
:preferences,
|
|
|
|
:message_id,
|
|
|
|
:from,
|
|
|
|
:to,
|
|
|
|
:cc
|
2015-04-27 23:19:26 +00:00
|
|
|
|
2015-08-30 20:15:23 +00:00
|
|
|
# fillup md5 of message id to search easier on very long message ids
|
|
|
|
def check_message_id_md5
|
2017-06-16 22:53:20 +00:00
|
|
|
return true if message_id.blank?
|
2015-08-30 20:15:23 +00:00
|
|
|
self.message_id_md5 = Digest::MD5.hexdigest(message_id.to_s)
|
|
|
|
end
|
|
|
|
|
2016-07-11 23:32:20 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
insert inline image urls to body
|
|
|
|
|
2017-03-10 05:34:51 +00:00
|
|
|
article_attributes = Ticket::Article.insert_urls(article_attributes)
|
2016-07-11 23:32:20 +00:00
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
article_attributes_with_body_and_urls
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
2017-03-10 05:34:51 +00:00
|
|
|
def self.insert_urls(article)
|
2017-06-16 22:53:20 +00:00
|
|
|
return article if article['attachments'].blank?
|
2017-03-10 05:34:51 +00:00
|
|
|
return article if article['content_type'] !~ %r{text/html}i
|
|
|
|
return article if article['body'] !~ /<img/i
|
|
|
|
|
2016-05-10 13:06:51 +00:00
|
|
|
inline_attachments = {}
|
2017-03-10 05:34:51 +00:00
|
|
|
article['body'].gsub!( /(<img[[:space:]](|.+?)src=")cid:(.+?)"(|.+?)>/im ) { |item|
|
|
|
|
tag_start = $1
|
|
|
|
cid = $3
|
|
|
|
tag_end = $4
|
2016-05-10 13:06:51 +00:00
|
|
|
replace = item
|
|
|
|
|
|
|
|
# look for attachment
|
2017-03-10 05:34:51 +00:00
|
|
|
article['attachments'].each { |file|
|
|
|
|
next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" )
|
|
|
|
replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}\"#{tag_end}>"
|
|
|
|
inline_attachments[file[:id]] = true
|
2016-05-10 13:06:51 +00:00
|
|
|
break
|
|
|
|
}
|
|
|
|
replace
|
|
|
|
}
|
|
|
|
new_attachments = []
|
2017-03-10 05:34:51 +00:00
|
|
|
article['attachments'].each { |file|
|
|
|
|
next if inline_attachments[file[:id]]
|
2016-05-10 13:06:51 +00:00
|
|
|
new_attachments.push file
|
|
|
|
}
|
|
|
|
article['attachments'] = new_attachments
|
|
|
|
article
|
|
|
|
end
|
|
|
|
|
2016-07-11 23:32:20 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get inline attachments of article
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
attachments = article.attachments_inline
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
[attachment1, attachment2, ...]
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def attachments_inline
|
|
|
|
inline_attachments = {}
|
2017-03-10 05:34:51 +00:00
|
|
|
body.gsub( /<img[[:space:]](|.+?)src="cid:(.+?)"(|.+?)>/im ) { |_item|
|
|
|
|
cid = $2
|
2016-07-11 23:32:20 +00:00
|
|
|
|
|
|
|
# look for attachment
|
|
|
|
attachments.each { |file|
|
2017-03-10 05:34:51 +00:00
|
|
|
next if !file.preferences['Content-ID'] || (file.preferences['Content-ID'] != cid && file.preferences['Content-ID'] != "<#{cid}>" )
|
2016-07-11 23:32:20 +00:00
|
|
|
inline_attachments[file.id] = true
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
new_attachments = []
|
|
|
|
attachments.each { |file|
|
|
|
|
next if !inline_attachments[file.id]
|
|
|
|
new_attachments.push file
|
|
|
|
}
|
|
|
|
new_attachments
|
|
|
|
end
|
|
|
|
|
2016-05-19 08:20:38 +00:00
|
|
|
def self.last_customer_agent_article(ticket_id)
|
|
|
|
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
|
|
|
|
end
|
|
|
|
|
2016-11-13 18:33:12 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get body as html
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
article.body_as_html
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def body_as_html
|
|
|
|
return '' if !body
|
|
|
|
return body if content_type && content_type =~ %r{text/html}i
|
|
|
|
body.text2html
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
get body as text
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
article.body_as_text
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def body_as_text
|
|
|
|
return '' if !body
|
|
|
|
return body if !content_type || content_type.empty? || content_type =~ %r{text/plain}i
|
|
|
|
body.html2text
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
get body as text with quote sign "> " at the beginning of each line
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
article.body_as_text
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def body_as_text_with_quote
|
|
|
|
body_as_text.word_wrap.message_quote
|
|
|
|
end
|
|
|
|
|
2016-12-20 23:07:47 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get article as raw (e. g. if it's a email, the raw email)
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
article.as_raw
|
|
|
|
|
|
|
|
returns:
|
|
|
|
|
|
|
|
file # Store
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def as_raw
|
|
|
|
list = Store.list(
|
|
|
|
object: 'Ticket::Article::Mail',
|
|
|
|
o_id: id,
|
|
|
|
)
|
2017-06-16 22:53:20 +00:00
|
|
|
return if list.blank?
|
2016-12-20 23:07:47 +00:00
|
|
|
list[0]
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
save article as raw (e. g. if it's a email, the raw email)
|
|
|
|
|
|
|
|
article = Ticket::Article.find(123)
|
|
|
|
article.save_as_raw(msg)
|
|
|
|
|
|
|
|
returns:
|
|
|
|
|
|
|
|
file # Store
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def save_as_raw(msg)
|
|
|
|
Store.add(
|
|
|
|
object: 'Ticket::Article::Mail',
|
|
|
|
o_id: id,
|
|
|
|
data: msg,
|
|
|
|
filename: "ticket-#{ticket.number}-#{id}.eml",
|
|
|
|
preferences: {},
|
|
|
|
created_by_id: created_by_id,
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
2017-02-01 11:48:50 +00:00
|
|
|
def sanitizeable?(attribute, _value)
|
|
|
|
return true if attribute != :body
|
|
|
|
return false if content_type.blank?
|
|
|
|
content_type =~ /html/i
|
|
|
|
end
|
|
|
|
|
2017-03-10 05:34:51 +00:00
|
|
|
=begin
|
|
|
|
|
|
|
|
get relation name of model based on params
|
|
|
|
|
|
|
|
model = Model.find(1)
|
|
|
|
attributes = model.attributes_with_association_names
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
hash with attributes, association ids, association names and relation name
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def attributes_with_association_names
|
|
|
|
attributes = super
|
|
|
|
attributes['attachments'] = []
|
|
|
|
attachments.each { |attachment|
|
|
|
|
item = {
|
|
|
|
id: attachment['id'],
|
|
|
|
filename: attachment['filename'],
|
|
|
|
size: attachment['size'],
|
|
|
|
preferences: attachment['preferences'],
|
|
|
|
}
|
|
|
|
attributes['attachments'].push item
|
|
|
|
}
|
|
|
|
Ticket::Article.insert_urls(attributes)
|
|
|
|
end
|
|
|
|
|
|
|
|
=begin
|
|
|
|
|
|
|
|
get relations of model based on params
|
|
|
|
|
|
|
|
model = Model.find(1)
|
|
|
|
attributes = model.attributes_with_association_ids
|
|
|
|
|
|
|
|
returns
|
|
|
|
|
|
|
|
hash with attributes and association ids
|
|
|
|
|
|
|
|
=end
|
|
|
|
|
|
|
|
def attributes_with_association_ids
|
|
|
|
attributes = super
|
|
|
|
attributes['attachments'] = []
|
|
|
|
attachments.each { |attachment|
|
|
|
|
item = {
|
|
|
|
id: attachment['id'],
|
|
|
|
filename: attachment['filename'],
|
|
|
|
size: attachment['size'],
|
|
|
|
preferences: attachment['preferences'],
|
|
|
|
}
|
|
|
|
attributes['attachments'].push item
|
|
|
|
}
|
2017-03-17 05:30:44 +00:00
|
|
|
if attributes['body'] && attributes['content_type'] =~ %r{text/html}i
|
|
|
|
attributes['body'] = HtmlSanitizer.dynamic_image_size(attributes['body'])
|
2017-03-17 05:27:50 +00:00
|
|
|
end
|
2017-03-10 05:34:51 +00:00
|
|
|
Ticket::Article.insert_urls(attributes)
|
|
|
|
end
|
|
|
|
|
2015-04-27 23:19:26 +00:00
|
|
|
private
|
|
|
|
|
2015-08-30 18:16:29 +00:00
|
|
|
# strip not wanted chars
|
2015-04-27 23:19:26 +00:00
|
|
|
def check_subject
|
2017-06-16 22:53:20 +00:00
|
|
|
return true if subject.blank?
|
2015-05-07 12:10:38 +00:00
|
|
|
subject.gsub!(/\s|\t|\r/, ' ')
|
2017-06-16 22:53:20 +00:00
|
|
|
true
|
2015-04-27 23:19:26 +00:00
|
|
|
end
|
2015-01-07 21:28:15 +00:00
|
|
|
|
2017-09-08 07:24:07 +00:00
|
|
|
# strip body length or raise exception
|
|
|
|
def check_body
|
|
|
|
return true if body.blank?
|
|
|
|
limit = 1_500_000
|
|
|
|
current_length = body.length
|
|
|
|
if body.length > limit
|
|
|
|
if ApplicationHandleInfo.current.present? && ApplicationHandleInfo.current.split('.')[1] == 'postmaster'
|
|
|
|
logger.warn "WARNING: cut string because of database length #{self.class}.body(#{limit} but is #{current_length})"
|
|
|
|
self.body = body[0, limit]
|
|
|
|
else
|
|
|
|
raise Exceptions::UnprocessableEntity, "body if article is to large, #{current_length} chars - only #{limit} allowed"
|
|
|
|
end
|
|
|
|
end
|
|
|
|
true
|
|
|
|
end
|
|
|
|
|
2017-01-31 17:13:45 +00:00
|
|
|
def history_log_attributes
|
|
|
|
{
|
|
|
|
related_o_id: self['ticket_id'],
|
|
|
|
related_history_object: 'Ticket',
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
# callback function to overwrite
|
|
|
|
# default history stream log attributes
|
|
|
|
# gets called from activity_stream_log
|
|
|
|
def activity_stream_log_attributes
|
|
|
|
{
|
|
|
|
group_id: Ticket.find(ticket_id).group_id,
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2015-04-27 23:19:26 +00:00
|
|
|
class Flag < ApplicationModel
|
|
|
|
end
|
2012-07-30 12:05:46 +00:00
|
|
|
|
2015-04-27 23:19:26 +00:00
|
|
|
class Sender < ApplicationModel
|
2017-05-02 15:21:13 +00:00
|
|
|
include ChecksLatestChangeObserved
|
2016-01-15 17:22:57 +00:00
|
|
|
validates :name, presence: true
|
2015-04-27 23:19:26 +00:00
|
|
|
end
|
2012-07-30 12:05:46 +00:00
|
|
|
|
2015-04-27 23:19:26 +00:00
|
|
|
class Type < ApplicationModel
|
2017-05-02 15:21:13 +00:00
|
|
|
include ChecksLatestChangeObserved
|
2016-01-15 17:22:57 +00:00
|
|
|
validates :name, presence: true
|
2012-07-30 12:05:46 +00:00
|
|
|
end
|
2015-04-27 14:15:29 +00:00
|
|
|
end
|