article.rb 8.2 KB

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