article.rb 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/
  2. class Ticket::Article < ApplicationModel
  3. include CanBeImported
  4. include HasActivityStreamLog
  5. include ChecksClientNotification
  6. include HasHistory
  7. include ChecksHtmlSanitized
  8. include CanCsvImport
  9. include CanCloneAttachments
  10. include HasObjectManagerAttributesValidation
  11. include Ticket::Article::Assets
  12. belongs_to :ticket, optional: true
  13. has_one :ticket_time_accounting, class_name: 'Ticket::TimeAccounting', foreign_key: :ticket_article_id, dependent: :destroy, inverse_of: :ticket_article
  14. belongs_to :type, class_name: 'Ticket::Article::Type', optional: true
  15. belongs_to :sender, class_name: 'Ticket::Article::Sender', optional: true
  16. belongs_to :created_by, class_name: 'User', optional: true
  17. belongs_to :updated_by, class_name: 'User', optional: true
  18. belongs_to :origin_by, class_name: 'User', optional: true
  19. before_create :check_subject, :check_body, :check_message_id_md5
  20. before_update :check_subject, :check_body, :check_message_id_md5
  21. after_destroy :store_delete
  22. store :preferences
  23. validates :ticket_id, presence: true
  24. validates :type_id, presence: true
  25. validates :sender_id, presence: true
  26. sanitized_html :body
  27. activity_stream_permission 'ticket.agent'
  28. activity_stream_attributes_ignored :type_id,
  29. :sender_id,
  30. :preferences
  31. history_attributes_ignored :type_id,
  32. :sender_id,
  33. :preferences,
  34. :message_id,
  35. :from,
  36. :to,
  37. :cc
  38. attr_accessor :should_clone_inline_attachments
  39. alias should_clone_inline_attachments? should_clone_inline_attachments
  40. # fillup md5 of message id to search easier on very long message ids
  41. def check_message_id_md5
  42. return true if message_id.blank?
  43. self.message_id_md5 = Digest::MD5.hexdigest(message_id.to_s)
  44. end
  45. =begin
  46. insert inline image urls to body
  47. article_attributes = Ticket::Article.insert_urls(article_attributes)
  48. returns
  49. article_attributes_with_body_and_urls
  50. =end
  51. def self.insert_urls(article)
  52. return article if article['attachments'].blank?
  53. return article if !article['content_type'].match?(%r{text/html}i)
  54. return article if article['body'] !~ /<img/i
  55. inline_attachments = {}
  56. article['body'].gsub!( /(<img[[:space:]](|.+?)src=")cid:(.+?)"(|.+?)>/im ) do |item|
  57. tag_start = $1
  58. cid = $3
  59. tag_end = $4
  60. replace = item
  61. # look for attachment
  62. article['attachments'].each do |file|
  63. next if !file[:preferences] || !file[:preferences]['Content-ID'] || (file[:preferences]['Content-ID'] != cid && file[:preferences]['Content-ID'] != "<#{cid}>" )
  64. replace = "#{tag_start}/api/v1/ticket_attachment/#{article['ticket_id']}/#{article['id']}/#{file[:id]}?view=inline\"#{tag_end}>"
  65. inline_attachments[file[:id]] = true
  66. break
  67. end
  68. replace
  69. end
  70. new_attachments = []
  71. article['attachments'].each do |file|
  72. next if inline_attachments[file[:id]]
  73. new_attachments.push file
  74. end
  75. article['attachments'] = new_attachments
  76. article
  77. end
  78. =begin
  79. get inline attachments of article
  80. article = Ticket::Article.find(123)
  81. attachments = article.attachments_inline
  82. returns
  83. [attachment1, attachment2, ...]
  84. =end
  85. def attachments_inline
  86. inline_attachments = {}
  87. body.gsub( /<img[[:space:]](|.+?)src="cid:(.+?)"(|.+?)>/im ) do |_item|
  88. cid = $2
  89. # look for attachment
  90. attachments.each do |file|
  91. content_id = file.preferences['Content-ID'] || file.preferences['content_id']
  92. next if content_id.blank? || (content_id != cid && content_id != "<#{cid}>" )
  93. inline_attachments[file.id] = true
  94. break
  95. end
  96. end
  97. new_attachments = []
  98. attachments.each do |file|
  99. next if !inline_attachments[file.id]
  100. new_attachments.push file
  101. end
  102. new_attachments
  103. end
  104. def self.last_customer_agent_article(ticket_id)
  105. sender = Ticket::Article::Sender.lookup(name: 'System')
  106. Ticket::Article.where('ticket_id = ? AND sender_id NOT IN (?)', ticket_id, sender.id).order(created_at: :desc).first
  107. end
  108. =begin
  109. get body as html
  110. article = Ticket::Article.find(123)
  111. article.body_as_html
  112. =end
  113. def body_as_html
  114. return '' if !body
  115. return body if content_type && content_type =~ %r{text/html}i
  116. body.text2html
  117. end
  118. =begin
  119. get body as text
  120. article = Ticket::Article.find(123)
  121. article.body_as_text
  122. =end
  123. def body_as_text
  124. return '' if !body
  125. return body if content_type.blank? || content_type =~ %r{text/plain}i
  126. body.html2text
  127. end
  128. =begin
  129. get body as text with quote sign "> " at the beginning of each line
  130. article = Ticket::Article.find(123)
  131. article.body_as_text
  132. =end
  133. def body_as_text_with_quote
  134. body_as_text.word_wrap.message_quote
  135. end
  136. =begin
  137. get article as raw (e. g. if it's a email, the raw email)
  138. article = Ticket::Article.find(123)
  139. article.as_raw
  140. returns:
  141. file # Store
  142. =end
  143. def as_raw
  144. list = Store.list(
  145. object: 'Ticket::Article::Mail',
  146. o_id: id,
  147. )
  148. return if list.blank?
  149. list[0]
  150. end
  151. =begin
  152. save article as raw (e. g. if it's a email, the raw email)
  153. article = Ticket::Article.find(123)
  154. article.save_as_raw(msg)
  155. returns:
  156. file # Store
  157. =end
  158. def save_as_raw(msg)
  159. Store.add(
  160. object: 'Ticket::Article::Mail',
  161. o_id: id,
  162. data: msg,
  163. filename: "ticket-#{ticket.number}-#{id}.eml",
  164. preferences: {},
  165. created_by_id: created_by_id,
  166. )
  167. end
  168. def sanitizeable?(attribute, _value)
  169. return true if attribute != :body
  170. return false if content_type.blank?
  171. content_type =~ /html/i
  172. end
  173. =begin
  174. get relation name of model based on params
  175. model = Model.find(1)
  176. attributes = model.attributes_with_association_names
  177. returns
  178. hash with attributes, association ids, association names and relation name
  179. =end
  180. def attributes_with_association_names
  181. attributes = super
  182. add_attachments_to_attributes(attributes)
  183. Ticket::Article.insert_urls(attributes)
  184. end
  185. =begin
  186. get relations of model based on params
  187. model = Model.find(1)
  188. attributes = model.attributes_with_association_ids
  189. returns
  190. hash with attributes and association ids
  191. =end
  192. def attributes_with_association_ids
  193. attributes = super
  194. add_attachments_to_attributes(attributes)
  195. if attributes['body'] && attributes['content_type'] =~ %r{text/html}i
  196. attributes['body'] = HtmlSanitizer.dynamic_image_size(attributes['body'])
  197. end
  198. Ticket::Article.insert_urls(attributes)
  199. end
  200. private
  201. def add_attachments_to_attributes(attributes)
  202. attributes['attachments'] = attachments.map(&:attributes_for_display)
  203. attributes
  204. end
  205. # strip not wanted chars
  206. def check_subject
  207. return true if subject.blank?
  208. subject.gsub!(/\s|\t|\r/, ' ')
  209. true
  210. end
  211. # strip body length or raise exception
  212. def check_body
  213. return true if body.blank?
  214. limit = 1_500_000
  215. current_length = body.length
  216. return true if body.length <= limit
  217. raise Exceptions::UnprocessableEntity, "body of article is too large, #{current_length} chars - only #{limit} allowed" if !ApplicationHandleInfo.postmaster?
  218. logger.warn "WARNING: cut string because of database length #{self.class}.body(#{limit} but is #{current_length})"
  219. self.body = body[0, limit]
  220. end
  221. def history_log_attributes
  222. {
  223. related_o_id: self['ticket_id'],
  224. related_history_object: 'Ticket',
  225. }
  226. end
  227. # callback function to overwrite
  228. # default history stream log attributes
  229. # gets called from activity_stream_log
  230. def activity_stream_log_attributes
  231. {
  232. group_id: Ticket.find(ticket_id).group_id,
  233. }
  234. end
  235. # delete attachments and mails of article
  236. def store_delete
  237. Store.remove(
  238. object: 'Ticket::Article',
  239. o_id: id,
  240. )
  241. Store.remove(
  242. object: 'Ticket::Article::Mail',
  243. o_id: id,
  244. )
  245. end
  246. end