Project

General

Profile

Feature #43484 » 0004-Detect-attachment-content-type-from-file-contents-in.patch

Go MAEDA, 2025-11-16 15:48

View differences:

app/assets/javascripts/attachments.js
120 120
  uploadUrl = uploadUrl + '?attachment_id=' + attachmentId;
121 121
  if (blob instanceof window.Blob) {
122 122
    uploadUrl += '&filename=' + encodeURIComponent(blob.name);
123
    uploadUrl += '&content_type=' + encodeURIComponent(blob.type);
124 123
  }
125 124

  
126 125
  return $.ajax(uploadUrl, {
app/controllers/attachments_controller.rb
108 108
    @attachment = Attachment.new(:file => raw_request_body)
109 109
    @attachment.author = User.current
110 110
    @attachment.filename = params[:filename].presence || Redmine::Utils.random_hex(16)
111
    @attachment.content_type = params[:content_type].presence
112 111
    saved = @attachment.save
113 112

  
114 113
    respond_to do |format|
app/models/attachment.rb
83 83
  cattr_accessor :thumbnails_storage_path
84 84
  @@thumbnails_storage_path = File.join(Rails.root, "tmp", "thumbnails")
85 85

  
86
  before_create :files_to_final_location
86
  before_create :set_content_type, :files_to_final_location
87 87
  after_commit :delete_from_disk, :on => :destroy
88 88
  after_commit :reuse_existing_file_if_possible, :on => :create
89 89
  after_rollback :delete_from_disk, :on => :create
......
118 118
        self.filename = @temp_file.original_filename
119 119
        self.filename.force_encoding("UTF-8")
120 120
      end
121
      if @temp_file.respond_to?(:content_type)
122
        self.content_type = @temp_file.content_type.to_s.chomp
123
      end
124 121
      self.filesize = @temp_file.size
125 122
    end
126 123
  end
......
134 131
    filename
135 132
  end
136 133

  
134
  def set_content_type
135
    return unless @temp_file
136

  
137
    io = @temp_file.respond_to?(:read) ? @temp_file : StringIO.new(@temp_file)
138
    self.content_type = Marcel::MimeType.for(io, name: filename)
139
    @temp_file.rewind if @temp_file.respond_to?(:rewind)
140
  end
141

  
137 142
  # Copies the temporary file to its final location
138 143
  # and computes its hash
139 144
  def files_to_final_location
......
157 162
      self.digest = sha.hexdigest
158 163
    end
159 164
    @temp_file = nil
160

  
161
    if content_type.blank? && filename.present?
162
      self.content_type = Redmine::MimeType.of(filename)
163
    end
164
    # Don't save the content type if it's longer than the authorized length
165
    if self.content_type && self.content_type.length > 255
166
      self.content_type = nil
167
    end
168 165
  end
169 166

  
170 167
  # Deletes the file from the file system if it's not referenced by other attachments
test/integration/api_test/attachments_test.rb
142 142
    assert_nil attachment.container
143 143
    assert_equal 2, attachment.author_id
144 144
    assert_equal 'File content'.size, attachment.filesize
145
    assert attachment.content_type.blank?
145
    assert_equal 'application/octet-stream', attachment.content_type
146 146
    assert attachment.filename.present?
147 147
    assert_match %r{\d+_[0-9a-z]+}, attachment.diskfile
148 148
    assert File.exist?(attachment.diskfile)
test/integration/attachments_test.rb
33 33
    assert_equal 'text/plain', attachment.content_type
34 34
  end
35 35

  
36
  def test_upload_should_accept_content_type_param
36
  def test_upload_should_detect_file_content_type
37 37
    log_user('jsmith', 'jsmith')
38 38
    assert_difference 'Attachment.count' do
39 39
      post(
40
        "/uploads.js?attachment_id=1&filename=foo&content_type=image/jpeg",
41
        :params => "File content",
40
        "/uploads.js?attachment_id=1&filename=foo",
41
        :params => "\xFF\xD8\xFF\xD9", # JPEG file signature
42 42
        :headers => {"CONTENT_TYPE" => 'application/octet-stream'})
43 43
      assert_response :success
44 44
    end
test/unit/attachment_test.rb
54 54
    assert_equal 59, File.size(a.diskfile)
55 55
  end
56 56

  
57
  def test_create_should_clear_content_type_if_too_long
57
  def test_create_should_not_trust_given_content_type_attribute
58 58
    a = Attachment.new(:container => Issue.find(1),
59
                       :file => uploaded_test_file("testfile.txt", "text/plain"),
59
                       :file => uploaded_test_file("testfile.txt", nil),
60 60
                       :author => User.find(1),
61 61
                       :content_type => 'a'*300)
62 62
    assert a.save
63 63
    a.reload
64
    assert_nil a.content_type
64
    assert_equal 'text/plain', a.content_type
65
  end
66

  
67
  def test_create_should_not_trust_client_declared_content_type
68
    a = Attachment.new(:container => Issue.find(1),
69
                        :file => uploaded_test_file("testfile.txt", "image/jpeg"),
70
                        :author => User.find(1))
71
    assert a.save
72
    a.reload
73
    assert_equal 'text/plain', a.content_type
65 74
  end
66 75

  
67 76
  def test_shorted_filename_if_too_long
(4-4/4)