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
2019-01-28 06:04:05 +00:00
include CanBeImported
2017-05-02 15:21:13 +00:00
include HasActivityStreamLog
include ChecksClientNotification
include HasHistory
include ChecksHtmlSanitized
2018-02-20 04:29:30 +00:00
include CanCsvImport
2017-01-31 17:13:45 +00:00
2018-04-26 08:55:53 +00:00
include Ticket :: Article :: ChecksAccess
2015-04-27 23:19:26 +00:00
include Ticket :: Article :: Assets
2018-04-12 14:57:37 +00:00
belongs_to :ticket
has_one :ticket_time_accounting , class_name : 'Ticket::TimeAccounting' , foreign_key : :ticket_article_id , dependent : :destroy , inverse_of : :ticket_article
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'
belongs_to :origin_by , class_name : 'User'
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
2017-11-17 10:41:44 +00:00
after_destroy :store_delete
2015-08-30 18:16:29 +00:00
2018-04-12 14:57:37 +00:00
store :preferences
2018-10-16 08:45:15 +00:00
validates :ticket_id , presence : true
validates :type_id , presence : true
validates :sender_id , presence : true
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
2019-02-10 08:40:55 +00:00
attr_accessor :should_clone_inline_attachments
alias should_clone_inline_attachments? should_clone_inline_attachments
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?
2018-10-09 06:17:41 +00:00
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-10-01 12:25:52 +00:00
article [ 'body' ] . gsub! ( / (<img[[:space:]](|.+?)src=")cid:(.+?)"(|.+?)> /im ) do | item |
2017-03-10 05:34:51 +00:00
tag_start = $1
cid = $3
tag_end = $4
2016-05-10 13:06:51 +00:00
replace = item
# look for attachment
2017-10-01 12:25:52 +00:00
article [ 'attachments' ] . each do | file |
2017-03-10 05:34:51 +00:00
next if ! file [ :preferences ] || ! file [ :preferences ] [ 'Content-ID' ] || ( file [ :preferences ] [ 'Content-ID' ] != cid && file [ :preferences ] [ 'Content-ID' ] != " < #{ cid } > " )
2018-10-09 06:17:41 +00:00
2019-02-14 05:42:41 +00:00
replace = " #{ tag_start } /api/v1/ticket_attachment/ #{ article [ 'ticket_id' ] } / #{ article [ 'id' ] } / #{ file [ :id ] } ?view=inline \" #{ tag_end } > "
2017-03-10 05:34:51 +00:00
inline_attachments [ file [ :id ] ] = true
2016-05-10 13:06:51 +00:00
break
2017-10-01 12:25:52 +00:00
end
2016-05-10 13:06:51 +00:00
replace
2017-10-01 12:25:52 +00:00
end
2016-05-10 13:06:51 +00:00
new_attachments = [ ]
2017-10-01 12:25:52 +00:00
article [ 'attachments' ] . each do | file |
2017-03-10 05:34:51 +00:00
next if inline_attachments [ file [ :id ] ]
2018-10-09 06:17:41 +00:00
2016-05-10 13:06:51 +00:00
new_attachments . push file
2017-10-01 12:25:52 +00:00
end
2016-05-10 13:06:51 +00:00
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-10-01 12:25:52 +00:00
body . gsub ( / <img[[:space:]](|.+?)src="cid:(.+?)"(|.+?)> /im ) do | _item |
2017-03-10 05:34:51 +00:00
cid = $2
2016-07-11 23:32:20 +00:00
# look for attachment
2017-10-01 12:25:52 +00:00
attachments . each do | 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 } > " )
2018-10-09 06:17:41 +00:00
2016-07-11 23:32:20 +00:00
inline_attachments [ file . id ] = true
break
2017-10-01 12:25:52 +00:00
end
end
2016-07-11 23:32:20 +00:00
new_attachments = [ ]
2017-10-01 12:25:52 +00:00
attachments . each do | file |
2016-07-11 23:32:20 +00:00
next if ! inline_attachments [ file . id ]
2018-10-09 06:17:41 +00:00
2016-07-11 23:32:20 +00:00
new_attachments . push file
2017-10-01 12:25:52 +00:00
end
2016-07-11 23:32:20 +00:00
new_attachments
end
2019-02-10 08:40:55 +00:00
= 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
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
2018-10-09 06:17:41 +00:00
2016-11-13 18:33:12 +00:00
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
2017-11-23 08:09:44 +00:00
return body if content_type . blank? || content_type =~ %r{ text/plain }i
2018-10-09 06:17:41 +00:00
2016-11-13 18:33:12 +00:00
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' ,
2018-12-19 17:31:51 +00:00
o_id : id ,
2016-12-20 23:07:47 +00:00
)
2017-06-16 22:53:20 +00:00
return if list . blank?
2018-10-09 06:17:41 +00:00
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 (
2018-12-19 17:31:51 +00:00
object : 'Ticket::Article::Mail' ,
o_id : id ,
data : msg ,
filename : " ticket- #{ ticket . number } - #{ id } .eml " ,
preferences : { } ,
2016-12-20 23:07:47 +00:00
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?
2018-10-09 06:17:41 +00:00
2017-02-01 11:48:50 +00:00
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
2018-05-03 14:09:33 +00:00
add_attachments_to_attributes ( attributes )
2017-03-10 05:34:51 +00:00
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
2018-05-03 14:09:33 +00:00
add_attachments_to_attributes ( attributes )
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
2018-05-03 14:09:33 +00:00
def add_attachments_to_attributes ( attributes )
attributes [ 'attachments' ] = attachments . map ( & :attributes_for_display )
attributes
end
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?
2018-10-09 06:17:41 +00:00
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?
2018-10-09 06:17:41 +00:00
2017-09-08 07:24:07 +00:00
limit = 1_500_000
current_length = body . length
2017-11-23 08:09:44 +00:00
return true if body . length < = limit
raise Exceptions :: UnprocessableEntity , " body if article is to large, #{ current_length } chars - only #{ limit } allowed " if ! ApplicationHandleInfo . postmaster?
logger . warn " WARNING: cut string because of database length #{ self . class } .body( #{ limit } but is #{ current_length } ) "
self . body = body [ 0 , limit ]
2017-09-08 07:24:07 +00:00
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
2017-11-17 10:41:44 +00:00
# delete attachments and mails of article
def store_delete
Store . remove (
object : 'Ticket::Article' ,
o_id : id ,
)
Store . remove (
object : 'Ticket::Article::Mail' ,
o_id : 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