has_rich_text.rb 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  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. return if raw.blank?
  33. parsed = Loofah.scrub_fragment(raw, HtmlSanitizer::CidToSrc.new).to_s
  34. parsed = HtmlSanitizer.strict(parsed)
  35. parsed = Loofah.scrub_fragment(parsed, HtmlSanitizer::RemoveLineBreaks.new).to_s
  36. (parsed, attachments_inline) = HtmlSanitizer.replace_inline_images(parsed, image_prefix)
  37. send(:"#{attr}=", parsed)
  38. self.has_rich_text_attachments_cache ||= []
  39. self.has_rich_text_attachments_cache += attachments_inline
  40. end
  41. def has_rich_text_commit_cache # rubocop:disable Naming/PredicateName
  42. return if has_rich_text_attachments_cache.blank?
  43. has_rich_text_attachments_cache.each do |attachment_cache|
  44. Store.create!(
  45. object: self.class.name,
  46. o_id: id,
  47. data: attachment_cache[:data],
  48. filename: attachment_cache[:filename],
  49. preferences: attachment_cache[:preferences],
  50. )
  51. end
  52. end
  53. def attributes_with_association_ids
  54. attrs = super
  55. self.class.has_rich_text_attributes.each do |attr|
  56. attrs[attr.to_s] = send(:"#{attr}_with_urls")
  57. end
  58. attrs
  59. end
  60. def has_rich_text_pickup_attachments # rubocop:disable Naming/PredicateName
  61. return if form_id.blank?
  62. self.attachments = Store.list(
  63. object: 'UploadCache',
  64. o_id: form_id,
  65. )
  66. end
  67. def has_rich_text_cleanup_unused_attachments # rubocop:disable Naming/PredicateName
  68. active_cids = has_rich_text_attributes.each_with_object([]) do |elem, memo|
  69. memo.concat HasRichText.extract_inline_cids(send(elem))
  70. end
  71. attachments
  72. .select { |file| HasRichText.attachment_inline?(file) }
  73. .reject { |file| active_cids.include? file.preferences&.dig('Content-ID') }
  74. .each { |file| Store.remove_item(file.id) }
  75. end
  76. class_methods do
  77. def has_rich_text(*attrs) # rubocop:disable Naming/PredicateName
  78. (self.has_rich_text_attributes += attrs.map(&:to_sym)).freeze
  79. attrs.each do |attr|
  80. define_method :"#{attr}_with_urls" do
  81. HasRichText.insert_urls(send(attr), attachments)
  82. end
  83. end
  84. end
  85. end
  86. class << self
  87. def insert_urls(raw, attachments)
  88. scrubber = Loofah::Scrubber.new do |node|
  89. next if node.name != 'img'
  90. next if !node['src']&.start_with?('cid:')
  91. cid = node['src'].sub(%r{^cid:}, '')
  92. lookup_cids = [cid, "<#{cid}>"]
  93. attachment = attachments.find do |file|
  94. lookup_cids.include? file.preferences&.dig('Content-ID')
  95. end
  96. next if !attachment
  97. node['cid'] = cid
  98. node['src'] = Rails.application.routes.url_helpers.attachment_path(attachment.id)
  99. end
  100. Loofah.scrub_fragment(raw, scrubber).to_s
  101. end
  102. def extract_inline_cids(raw)
  103. inline_cids = []
  104. scrubber = Loofah::Scrubber.new do |node|
  105. next if node.name != 'img'
  106. next if !node['src']&.start_with? 'cid:'
  107. cid = node['src'].sub(%r{^cid:}, '')
  108. inline_cids << cid
  109. end
  110. Loofah.scrub_fragment(raw, scrubber)
  111. inline_cids
  112. end
  113. end
  114. end