has_rich_text.rb 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. module HasRichText
  3. extend ActiveSupport::Concern
  4. included do
  5. class_attribute :has_rich_text_attributes
  6. self.has_rich_text_attributes = [].freeze
  7. attr_accessor :has_rich_text_attachments_cache
  8. attr_accessor :form_id
  9. before_save :has_rich_text_parse
  10. after_save :has_rich_text_commit_cache
  11. after_save :has_rich_text_pickup_attachments
  12. after_save :has_rich_text_cleanup_unused_attachments
  13. end
  14. =begin
  15. Checks if file is used inline
  16. @param store_object [Store] attachment to evaluate
  17. @return [Bool] if attachment is inline
  18. @example
  19. store_object = Store.first
  20. HasRichText.attachment_inline?(::CanAssets.reduce(list, {})
  21. =end
  22. def self.attachment_inline?(store_object)
  23. store_object.preferences&.dig('Content-Disposition') == 'inline'
  24. end
  25. private
  26. def has_rich_text_parse # rubocop:disable Naming/PredicateName
  27. has_rich_text_attributes.each { |attr| has_rich_text_parse_attribute(attr) }
  28. end
  29. def has_rich_text_parse_attribute(attr) # rubocop:disable Naming/PredicateName
  30. image_prefix = "#{self.class.name}_#{attr}"
  31. raw = send(attr)
  32. scrubber = Loofah::Scrubber.new do |node|
  33. next if node.name != 'img'
  34. next if !(cid = node.delete 'cid')
  35. node['src'] = "cid:#{cid}"
  36. end
  37. parsed = Loofah.scrub_fragment(raw, scrubber).to_s
  38. parsed = HtmlSanitizer.strict(parsed)
  39. line_breaks = ["\n", "\r", "\r\n"]
  40. scrubber_cleaner = Loofah::Scrubber.new(direction: :bottom_up) do |node|
  41. case node.name
  42. when 'span'
  43. node.children.reject { |t| line_breaks.include?(t.text) }.each { |child| node.before child }
  44. node.remove
  45. when 'div'
  46. node.children.to_a.select { |t| t.text.match?(%r{\A([\n\r]+)\z}) }.each(&:remove)
  47. node.remove if node.children.none? && node.classes.none?
  48. end
  49. end
  50. parsed = Loofah.scrub_fragment(parsed, scrubber_cleaner).to_s
  51. (parsed, attachments_inline) = HtmlSanitizer.replace_inline_images(parsed, image_prefix)
  52. send(:"#{attr}=", parsed)
  53. self.has_rich_text_attachments_cache ||= []
  54. self.has_rich_text_attachments_cache += attachments_inline
  55. end
  56. def has_rich_text_commit_cache # rubocop:disable Naming/PredicateName
  57. return if has_rich_text_attachments_cache.blank?
  58. has_rich_text_attachments_cache.each do |attachment_cache|
  59. Store.create!(
  60. object: self.class.name,
  61. o_id: id,
  62. data: attachment_cache[:data],
  63. filename: attachment_cache[:filename],
  64. preferences: attachment_cache[:preferences],
  65. )
  66. end
  67. end
  68. def attributes_with_association_ids
  69. attrs = super
  70. self.class.has_rich_text_attributes.each do |attr|
  71. attrs[attr.to_s] = send(:"#{attr}_with_urls")
  72. end
  73. attrs
  74. end
  75. def has_rich_text_pickup_attachments # rubocop:disable Naming/PredicateName
  76. return if form_id.blank?
  77. self.attachments = Store.list(
  78. object: 'UploadCache',
  79. o_id: form_id,
  80. )
  81. end
  82. def has_rich_text_cleanup_unused_attachments # rubocop:disable Naming/PredicateName
  83. active_cids = has_rich_text_attributes.each_with_object([]) do |elem, memo|
  84. memo.concat self.class.has_rich_text_inline_cids(self, elem)
  85. end
  86. attachments
  87. .select { |file| HasRichText.attachment_inline?(file) }
  88. .reject { |file| active_cids.include? file.preferences&.dig('Content-ID') }
  89. .each { |file| Store.remove_item(file.id) }
  90. end
  91. class_methods do
  92. def has_rich_text(*attrs) # rubocop:disable Naming/PredicateName
  93. (self.has_rich_text_attributes += attrs.map(&:to_sym)).freeze
  94. attrs.each do |attr|
  95. define_method :"#{attr}_with_urls" do
  96. self.class.has_rich_text_insert_urls(self, attr)
  97. end
  98. end
  99. end
  100. def has_rich_text_insert_urls(object, attr) # rubocop:disable Naming/PredicateName
  101. raw = object.send(attr)
  102. attachments = object.attachments
  103. scrubber = Loofah::Scrubber.new do |node|
  104. next if node.name != 'img'
  105. next if !node['src']&.start_with?('cid:')
  106. cid = node['src'].sub(%r{^cid:}, '')
  107. lookup_cids = [cid, "<#{cid}>"]
  108. attachment = attachments.find do |file|
  109. lookup_cids.include? file.preferences&.dig('Content-ID')
  110. end
  111. next if !attachment
  112. node['cid'] = cid
  113. node['src'] = Rails.application.routes.url_helpers.attachment_path(attachment.id)
  114. end
  115. Loofah.scrub_fragment(raw, scrubber).to_s
  116. end
  117. def has_rich_text_inline_cids(object, attr) # rubocop:disable Naming/PredicateName
  118. raw = object.send(attr)
  119. inline_cids = []
  120. scrubber = Loofah::Scrubber.new do |node|
  121. next if node.name != 'img'
  122. next if !node['src']&.start_with? 'cid:'
  123. cid = node['src'].sub(%r{^cid:}, '')
  124. inline_cids << cid
  125. end
  126. Loofah.scrub_fragment(raw, scrubber)
  127. inline_cids
  128. end
  129. end
  130. end