2021-06-01 12:20:20 +00:00
# Copyright (C) 2012-2021 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
2020-04-20 09:47:45 +00:00
include CanCloneAttachments
2021-08-17 16:37:16 +00:00
include HasObjectManagerAttributes
2017-01-31 17:13:45 +00:00
2015-04-27 23:19:26 +00:00
include Ticket :: Article :: Assets
2021-03-01 08:18:40 +00:00
include Ticket :: Article :: EnqueueCommunicateEmailJob
include Ticket :: Article :: EnqueueCommunicateFacebookJob
include Ticket :: Article :: EnqueueCommunicateSmsJob
include Ticket :: Article :: EnqueueCommunicateTelegramJob
include Ticket :: Article :: EnqueueCommunicateTwitterJob
2021-02-15 13:55:00 +00:00
include Ticket :: Article :: HasTicketContactAttributesImpact
2021-03-01 08:18:40 +00:00
include Ticket :: Article :: ResetsTicketState
# AddsMetadataGeneral depends on AddsMetadataOriginById, so load that first
include Ticket :: Article :: AddsMetadataOriginById
include Ticket :: Article :: AddsMetadataGeneral
include Ticket :: Article :: AddsMetadataEmail
2015-04-27 23:19:26 +00:00
2021-05-20 06:59:02 +00:00
include HasTransactionDispatcher
2019-07-04 11:16:55 +00:00
belongs_to :ticket , optional : true
2018-04-12 14:57:37 +00:00
has_one :ticket_time_accounting , class_name : 'Ticket::TimeAccounting' , foreign_key : :ticket_article_id , dependent : :destroy , inverse_of : :ticket_article
2019-07-04 11:16:55 +00:00
belongs_to :type , class_name : 'Ticket::Article::Type' , optional : true
belongs_to :sender , class_name : 'Ticket::Article::Sender' , optional : true
belongs_to :created_by , class_name : 'User' , optional : true
belongs_to :updated_by , class_name : 'User' , optional : true
belongs_to :origin_by , class_name : 'User' , optional : true
2018-04-12 14:57:37 +00:00
2021-03-16 08:59:32 +00:00
before_validation :check_mentions , on : :create
before_save :touch_ticket_if_needed
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
2020-10-23 11:53:34 +00:00
after_destroy :store_delete , :update_time_units
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
2020-05-11 12:49:22 +00:00
2019-02-10 08:40:55 +00:00
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?
2019-09-16 09:21:10 +00:00
return article if ! article [ 'content_type' ] . match? ( %r{ text/html }i )
2021-05-12 11:37:44 +00:00
return article if article [ 'body' ] !~ %r{ <img }i
2017-03-10 05:34:51 +00:00
2016-05-10 13:06:51 +00:00
inline_attachments = { }
2021-07-16 13:38:01 +00:00
article [ 'body' ] . gsub! ( %r{ (<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 |
2021-07-16 13:38:01 +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 = { }
2021-07-16 13:38:01 +00:00
body . gsub ( %r{ <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 |
2019-03-11 17:17:08 +00:00
content_id = file . preferences [ 'Content-ID' ] || file . preferences [ 'content_id' ]
2021-07-16 13:38:01 +00:00
next if content_id . blank? || ( content_id != cid && 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
2019-02-10 08:40:55 +00:00
end
2016-05-19 08:20:38 +00:00
def self . last_customer_agent_article ( ticket_id )
sender = Ticket :: Article :: Sender . lookup ( name : 'System' )
2019-04-07 15:23:03 +00:00
Ticket :: Article . where ( 'ticket_id = ? AND sender_id NOT IN (?)' , ticket_id , sender . id ) . order ( created_at : :desc ) . first
2016-05-19 08:20:38 +00:00
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
2021-05-12 11:37:44 +00:00
content_type =~ %r{ html }i
2017-02-01 11:48:50 +00:00
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
2021-05-12 11:37:44 +00:00
subject . gsub! ( %r{ \ 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
2020-07-10 14:54:04 +00:00
raise Exceptions :: UnprocessableEntity , " body of article is too large, #{ current_length } chars - only #{ limit } allowed " if ! ApplicationHandleInfo . postmaster? && ! Setting . get ( 'import_mode' )
2017-11-23 08:09:44 +00:00
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
2021-03-16 08:59:32 +00:00
def check_mentions
begin
mention_user_ids = Nokogiri :: HTML ( body ) . css ( 'a[data-mention-user-id]' ) . map do | link |
link [ 'data-mention-user-id' ]
end
rescue = > e
Rails . logger . error " Can't parse body ' #{ body } ' as HTML for extracting Mentions. "
Rails . logger . error e
return
end
return if mention_user_ids . blank?
2021-04-01 07:20:00 +00:00
return if ApplicationHandleInfo . postmaster? && ! MentionPolicy . new ( updated_by , Mention . new ) . create?
2021-03-16 08:59:32 +00:00
raise " User #{ updated_by_id } has no permission to mention other Users! " if ! MentionPolicy . new ( updated_by , Mention . new ) . create?
user_ids = User . where ( id : mention_user_ids ) . pluck ( :id )
user_ids . each do | user_id |
Mention . where ( mentionable : ticket , user_id : user_id ) . first_or_create ( mentionable : ticket , user_id : user_id )
end
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
2020-06-02 11:01:16 +00:00
2020-10-23 11:53:34 +00:00
# recalculate time accounting
def update_time_units
Ticket :: TimeAccounting . update_ticket ( ticket )
end
2020-08-31 04:27:27 +00:00
def touch_ticket_if_needed
return if ! internal_changed?
ticket & . touch # rubocop:disable Rails/SkipsModelValidations
end
2015-04-27 14:15:29 +00:00
end