has_rich_text.rb 4.8 KB

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