mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	 9a7318a9db
			
		
	
	9a7318a9db
	
	
	
		
			
			# Pull Request Template
## Description
Fixes
https://linear.app/chatwoot/issue/CW-5411/actionviewtemplateerror-activestorageunrepresentableerror
###  Problem
API endpoints return 500 errors when conversations contain image
attachments that can't be processed by ActiveStorage (e.g., files with
non-ASCII filenames, corrupted images, or malicious XSS filenames).
Root Cause: Commit 6cab74139 removed the representable? safety check
from thumb_url, causing `ActiveStorage::UnrepresentableError` to bubble
up and crash the API when it encountered a malformed image file.
Fix: Rescue `thumb_url` method to catch UnrepresentableError and return
an empty string while logging problematic names for future debugging.
This ensures the messages/attachments api does not break due to a single
corrupted image file.
## Type of change
- [x] Bug fix (non-breaking change which fixes an issue)
## How Has This Been Tested?
- Added specs
## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented on my code, particularly in hard-to-understand
areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream
modules
		
	
		
			
				
	
	
		
			178 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			4.7 KiB
		
	
	
	
		
			Ruby
		
	
	
	
	
	
| # == Schema Information
 | |
| #
 | |
| # Table name: attachments
 | |
| #
 | |
| #  id               :integer          not null, primary key
 | |
| #  coordinates_lat  :float            default(0.0)
 | |
| #  coordinates_long :float            default(0.0)
 | |
| #  extension        :string
 | |
| #  external_url     :string
 | |
| #  fallback_title   :string
 | |
| #  file_type        :integer          default("image")
 | |
| #  meta             :jsonb
 | |
| #  created_at       :datetime         not null
 | |
| #  updated_at       :datetime         not null
 | |
| #  account_id       :integer          not null
 | |
| #  message_id       :integer          not null
 | |
| #
 | |
| # Indexes
 | |
| #
 | |
| #  index_attachments_on_account_id  (account_id)
 | |
| #  index_attachments_on_message_id  (message_id)
 | |
| #
 | |
| 
 | |
| class Attachment < ApplicationRecord
 | |
|   include Rails.application.routes.url_helpers
 | |
| 
 | |
|   ACCEPTABLE_FILE_TYPES = %w[
 | |
|     text/csv text/plain text/rtf
 | |
|     application/json application/pdf
 | |
|     application/zip application/x-7z-compressed application/vnd.rar application/x-tar
 | |
|     application/msword application/vnd.ms-excel application/vnd.ms-powerpoint application/rtf
 | |
|     application/vnd.oasis.opendocument.text
 | |
|     application/vnd.openxmlformats-officedocument.presentationml.presentation
 | |
|     application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
 | |
|     application/vnd.openxmlformats-officedocument.wordprocessingml.document
 | |
|   ].freeze
 | |
|   belongs_to :account
 | |
|   belongs_to :message
 | |
|   has_one_attached :file
 | |
|   validate :acceptable_file
 | |
|   validates :external_url, length: { maximum: Limits::URL_LENGTH_LIMIT }
 | |
|   enum file_type: { :image => 0, :audio => 1, :video => 2, :file => 3, :location => 4, :fallback => 5, :share => 6, :story_mention => 7,
 | |
|                     :contact => 8, :ig_reel => 9 }
 | |
| 
 | |
|   def push_event_data
 | |
|     return unless file_type
 | |
| 
 | |
|     base_data.merge(metadata_for_file_type)
 | |
|   end
 | |
| 
 | |
|   # NOTE: the URl returned does a 301 redirect to the actual file
 | |
|   def file_url
 | |
|     file.attached? ? url_for(file) : ''
 | |
|   end
 | |
| 
 | |
|   # NOTE: for External services use this methods since redirect doesn't work effectively in a lot of cases
 | |
|   def download_url
 | |
|     ActiveStorage::Current.url_options = Rails.application.routes.default_url_options if ActiveStorage::Current.url_options.blank?
 | |
|     file.attached? ? file.blob.url : ''
 | |
|   end
 | |
| 
 | |
|   def thumb_url
 | |
|     return '' unless file.attached? && image?
 | |
| 
 | |
|     begin
 | |
|       url_for(file.representation(resize_to_fill: [250, nil]))
 | |
|     rescue ActiveStorage::UnrepresentableError => e
 | |
|       Rails.logger.warn "Unrepresentable image attachment: #{id} (#{file.filename}) - #{e.message}"
 | |
|       ''
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def with_attached_file?
 | |
|     [:image, :audio, :video, :file].include?(file_type.to_sym)
 | |
|   end
 | |
| 
 | |
|   private
 | |
| 
 | |
|   def metadata_for_file_type
 | |
|     case file_type.to_sym
 | |
|     when :location
 | |
|       location_metadata
 | |
|     when :fallback
 | |
|       fallback_data
 | |
|     when :contact
 | |
|       contact_metadata
 | |
|     when :audio
 | |
|       audio_metadata
 | |
|     else
 | |
|       file_metadata
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def audio_metadata
 | |
|     audio_file_data = base_data.merge(file_metadata)
 | |
|     audio_file_data.merge(
 | |
|       {
 | |
|         transcribed_text: meta&.[]('transcribed_text') || ''
 | |
|       }
 | |
|     )
 | |
|   end
 | |
| 
 | |
|   def file_metadata
 | |
|     metadata = {
 | |
|       extension: extension,
 | |
|       data_url: file_url,
 | |
|       thumb_url: thumb_url,
 | |
|       file_size: file.byte_size,
 | |
|       width: file.metadata[:width],
 | |
|       height: file.metadata[:height]
 | |
|     }
 | |
| 
 | |
|     metadata[:data_url] = metadata[:thumb_url] = external_url if message.inbox.instagram? && message.incoming?
 | |
|     metadata
 | |
|   end
 | |
| 
 | |
|   def location_metadata
 | |
|     {
 | |
|       coordinates_lat: coordinates_lat,
 | |
|       coordinates_long: coordinates_long,
 | |
|       fallback_title: fallback_title,
 | |
|       data_url: external_url
 | |
|     }
 | |
|   end
 | |
| 
 | |
|   def fallback_data
 | |
|     {
 | |
|       fallback_title: fallback_title,
 | |
|       data_url: external_url
 | |
|     }
 | |
|   end
 | |
| 
 | |
|   def base_data
 | |
|     {
 | |
|       id: id,
 | |
|       message_id: message_id,
 | |
|       file_type: file_type,
 | |
|       account_id: account_id
 | |
|     }
 | |
|   end
 | |
| 
 | |
|   def contact_metadata
 | |
|     {
 | |
|       fallback_title: fallback_title,
 | |
|       meta: meta || {}
 | |
|     }
 | |
|   end
 | |
| 
 | |
|   def should_validate_file?
 | |
|     return unless file.attached?
 | |
|     # we are only limiting attachment types in case of website widget
 | |
|     return unless message.inbox.channel_type == 'Channel::WebWidget'
 | |
| 
 | |
|     true
 | |
|   end
 | |
| 
 | |
|   def acceptable_file
 | |
|     return unless should_validate_file?
 | |
| 
 | |
|     validate_file_size(file.byte_size)
 | |
|     validate_file_content_type(file.content_type)
 | |
|   end
 | |
| 
 | |
|   def validate_file_content_type(file_content_type)
 | |
|     errors.add(:file, 'type not supported') unless media_file?(file_content_type) || ACCEPTABLE_FILE_TYPES.include?(file_content_type)
 | |
|   end
 | |
| 
 | |
|   def validate_file_size(byte_size)
 | |
|     errors.add(:file, 'size is too big') if byte_size > 40.megabytes
 | |
|   end
 | |
| 
 | |
|   def media_file?(file_content_type)
 | |
|     file_content_type.start_with?('image/', 'video/', 'audio/')
 | |
|   end
 | |
| end
 | |
| 
 | |
| Attachment.include_mod_with('Concerns::Attachment')
 |