paperclip 3.5.4 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of paperclip might be problematic. Click here for more details.

Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/LICENSE +1 -3
  4. data/NEWS +25 -21
  5. data/README.md +35 -1
  6. data/features/step_definitions/attachment_steps.rb +2 -0
  7. data/features/step_definitions/rails_steps.rb +1 -0
  8. data/lib/paperclip.rb +1 -0
  9. data/lib/paperclip/attachment.rb +25 -1
  10. data/lib/paperclip/callbacks.rb +1 -1
  11. data/lib/paperclip/content_type_detector.rb +1 -13
  12. data/lib/paperclip/errors.rb +5 -0
  13. data/lib/paperclip/has_attached_file.rb +5 -0
  14. data/lib/paperclip/io_adapters/abstract_adapter.rb +1 -1
  15. data/lib/paperclip/io_adapters/attachment_adapter.rb +4 -4
  16. data/lib/paperclip/io_adapters/data_uri_adapter.rb +4 -9
  17. data/lib/paperclip/io_adapters/stringio_adapter.rb +10 -8
  18. data/lib/paperclip/media_type_spoof_detector.rb +36 -0
  19. data/lib/paperclip/tempfile_factory.rb +5 -1
  20. data/lib/paperclip/validators.rb +6 -1
  21. data/lib/paperclip/validators/attachment_content_type_validator.rb +4 -0
  22. data/lib/paperclip/validators/attachment_file_name_validator.rb +80 -0
  23. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +29 -0
  24. data/lib/paperclip/validators/attachment_presence_validator.rb +4 -0
  25. data/lib/paperclip/validators/attachment_size_validator.rb +4 -0
  26. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +27 -0
  27. data/lib/paperclip/version.rb +1 -1
  28. data/test/attachment_definitions_test.rb +1 -0
  29. data/test/attachment_test.rb +40 -43
  30. data/test/content_type_detector_test.rb +0 -10
  31. data/test/fixtures/empty.html +1 -0
  32. data/test/has_attached_file_test.rb +3 -1
  33. data/test/helper.rb +11 -4
  34. data/test/io_adapters/abstract_adapter_test.rb +1 -0
  35. data/test/io_adapters/attachment_adapter_test.rb +1 -1
  36. data/test/io_adapters/data_uri_adapter_test.rb +2 -2
  37. data/test/io_adapters/file_adapter_test.rb +0 -11
  38. data/test/io_adapters/http_url_proxy_adapter_test.rb +2 -3
  39. data/test/io_adapters/stringio_adapter_test.rb +1 -1
  40. data/test/matchers/have_attached_file_matcher_test.rb +3 -2
  41. data/test/matchers/validate_attachment_content_type_matcher_test.rb +13 -12
  42. data/test/matchers/validate_attachment_presence_matcher_test.rb +8 -7
  43. data/test/matchers/validate_attachment_size_matcher_test.rb +12 -11
  44. data/test/media_type_spoof_detector_test.rb +28 -0
  45. data/test/meta_class_test.rb +2 -2
  46. data/test/schema_test.rb +6 -0
  47. data/test/storage/fog_test.rb +4 -4
  48. data/test/storage/s3_test.rb +32 -31
  49. data/test/tempfile_factory_test.rb +13 -1
  50. data/test/validators/attachment_file_name_validator_test.rb +162 -0
  51. data/test/validators/attachment_presence_validator_test.rb +1 -1
  52. data/test/validators/media_type_spoof_detection_validator_test.rb +12 -0
  53. data/test/validators_test.rb +43 -3
  54. metadata +14 -2
@@ -1,8 +1,11 @@
1
1
  require 'active_model'
2
2
  require 'active_support/concern'
3
3
  require 'paperclip/validators/attachment_content_type_validator'
4
+ require 'paperclip/validators/attachment_file_name_validator'
4
5
  require 'paperclip/validators/attachment_presence_validator'
5
6
  require 'paperclip/validators/attachment_size_validator'
7
+ require 'paperclip/validators/media_type_spoof_detection_validator'
8
+ require 'paperclip/validators/attachment_file_type_ignorance_validator'
6
9
 
7
10
  module Paperclip
8
11
  module Validators
@@ -13,6 +16,8 @@ module Paperclip
13
16
  include HelperMethods
14
17
  end
15
18
 
19
+ ::Paperclip::REQUIRED_VALIDATORS = [AttachmentFileNameValidator, AttachmentContentTypeValidator, AttachmentFileTypeIgnoranceValidator]
20
+
16
21
  module ClassMethods
17
22
  # This method is a shortcut to validator classes that is in
18
23
  # "Attachment...Validator" format. It is almost the same thing as the
@@ -39,7 +44,7 @@ module Paperclip
39
44
  local_options = attributes + [validator_options]
40
45
  conditional_options = options.slice(:if, :unless)
41
46
  local_options.last.merge!(conditional_options)
42
- send(:"validates_attachment_#{validator_kind}", *local_options)
47
+ send(Paperclip::Validators.const_get(constant.to_s).helper_method_name, *local_options)
43
48
  end
44
49
  end
45
50
  end
@@ -6,6 +6,10 @@ module Paperclip
6
6
  super
7
7
  end
8
8
 
9
+ def self.helper_method_name
10
+ :validates_attachment_content_type
11
+ end
12
+
9
13
  def validate_each(record, attribute, value)
10
14
  base_attribute = attribute.to_sym
11
15
  attribute = "#{attribute}_content_type".to_sym
@@ -0,0 +1,80 @@
1
+ module Paperclip
2
+ module Validators
3
+ class AttachmentFileNameValidator < ActiveModel::EachValidator
4
+ def initialize(options)
5
+ options[:allow_nil] = true unless options.has_key?(:allow_nil)
6
+ super
7
+ end
8
+
9
+ def self.helper_method_name
10
+ :validates_attachment_file_name
11
+ end
12
+
13
+ def validate_each(record, attribute, value)
14
+ base_attribute = attribute.to_sym
15
+ attribute = "#{attribute}_file_name".to_sym
16
+ value = record.send :read_attribute_for_validation, attribute
17
+
18
+ return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
19
+
20
+ validate_whitelist(record, attribute, value)
21
+ validate_blacklist(record, attribute, value)
22
+
23
+ if record.errors.include? attribute
24
+ record.errors[attribute].each do |error|
25
+ record.errors.add base_attribute, error
26
+ end
27
+ end
28
+ end
29
+
30
+ def validate_whitelist(record, attribute, value)
31
+ if allowed.present? && allowed.none? { |type| type === value }
32
+ mark_invalid record, attribute, allowed
33
+ end
34
+ end
35
+
36
+ def validate_blacklist(record, attribute, value)
37
+ if forbidden.present? && forbidden.any? { |type| type === value }
38
+ mark_invalid record, attribute, forbidden
39
+ end
40
+ end
41
+
42
+ def mark_invalid(record, attribute, patterns)
43
+ record.errors.add attribute, :invalid, options.merge(:names => patterns.join(', '))
44
+ end
45
+
46
+ def allowed
47
+ [options[:matches]].flatten.compact
48
+ end
49
+
50
+ def forbidden
51
+ [options[:not]].flatten.compact
52
+ end
53
+
54
+ def check_validity!
55
+ unless options.has_key?(:matches) || options.has_key?(:not)
56
+ raise ArgumentError, "You must pass in either :matches or :not to the validator"
57
+ end
58
+ end
59
+ end
60
+
61
+ module HelperMethods
62
+ # Places ActiveModel validations on the name of the file
63
+ # assigned. The possible options are:
64
+ # * +matches+: Allowed filename patterns as Regexps. Can be a single one
65
+ # or an array.
66
+ # * +not+: Forbidden file name patterns, specified the same was as +matches+.
67
+ # * +message+: The message to display when the uploaded file has an invalid
68
+ # name.
69
+ # * +if+: A lambda or name of an instance method. Validation will only
70
+ # be run is this lambda or method returns true.
71
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
72
+ def validates_attachment_file_name(*attr_names)
73
+ options = _merge_attributes(attr_names)
74
+ validates_with AttachmentFileNameValidator, options.dup
75
+ validate_before_processing AttachmentFileNameValidator, options.dup
76
+ end
77
+ end
78
+ end
79
+ end
80
+
@@ -0,0 +1,29 @@
1
+ require 'active_model/validations/presence'
2
+
3
+ module Paperclip
4
+ module Validators
5
+ class AttachmentFileTypeIgnoranceValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ # This doesn't do anything. It's just to mark that you don't care about
8
+ # the file_names or content_types of your incoming attachments.
9
+ end
10
+
11
+ def self.helper_method_name
12
+ :do_not_validate_attachment_file_type
13
+ end
14
+ end
15
+
16
+ module HelperMethods
17
+ # Places ActiveModel validations on the presence of a file.
18
+ # Options:
19
+ # * +if+: A lambda or name of an instance method. Validation will only
20
+ # be run if this lambda or method returns true.
21
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
22
+ def do_not_validate_attachment_file_type(*attr_names)
23
+ options = _merge_attributes(attr_names)
24
+ validates_with AttachmentFileTypeIgnoranceValidator, options.dup
25
+ end
26
+ end
27
+ end
28
+ end
29
+
@@ -8,6 +8,10 @@ module Paperclip
8
8
  record.errors.add(attribute, :blank, options)
9
9
  end
10
10
  end
11
+
12
+ def self.helper_method_name
13
+ :validates_attachment_presence
14
+ end
11
15
  end
12
16
 
13
17
  module HelperMethods
@@ -10,6 +10,10 @@ module Paperclip
10
10
  super
11
11
  end
12
12
 
13
+ def self.helper_method_name
14
+ :validates_attachment_size
15
+ end
16
+
13
17
  def validate_each(record, attr_name, value)
14
18
  base_attr_name = attr_name
15
19
  attr_name = "#{attr_name}_file_size".to_sym
@@ -0,0 +1,27 @@
1
+ require 'active_model/validations/presence'
2
+
3
+ module Paperclip
4
+ module Validators
5
+ class MediaTypeSpoofDetectionValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, value)
7
+ adapter = Paperclip.io_adapters.for(value)
8
+ if Paperclip::MediaTypeSpoofDetector.using(adapter, value.original_filename).spoofed?
9
+ record.errors.add(attribute, :spoofed_media_type)
10
+ end
11
+ end
12
+ end
13
+
14
+ module HelperMethods
15
+ # Places ActiveModel validations on the presence of a file.
16
+ # Options:
17
+ # * +if+: A lambda or name of an instance method. Validation will only
18
+ # be run if this lambda or method returns true.
19
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
20
+ def validates_media_type_spoof_detection(*attr_names)
21
+ options = _merge_attributes(attr_names)
22
+ validates_with MediaTypeSpoofDetectionValidator, options.dup
23
+ validate_before_processing MediaTypeSpoofDetectionValidator, options.dup
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,3 +1,3 @@
1
1
  module Paperclip
2
- VERSION = "3.5.4" unless defined? Paperclip::VERSION
2
+ VERSION = "4.0.0" unless defined? Paperclip::VERSION
3
3
  end
@@ -5,6 +5,7 @@ class AttachmentDefinitionsTest < Test::Unit::TestCase
5
5
  reset_class "Dummy"
6
6
  Dummy.has_attached_file :avatar, {:path => "abc"}
7
7
  Dummy.has_attached_file :other_attachment, {:url => "123"}
8
+ Dummy.do_not_validate_attachment_file_type :avatar
8
9
  expected = {:avatar => {:path => "abc"}, :other_attachment => {:url => "123"}}
9
10
 
10
11
  assert_equal expected, Dummy.attachment_definitions
@@ -165,7 +165,7 @@ class AttachmentTest < Test::Unit::TestCase
165
165
  :default_style => 'default style',
166
166
  :url_generator => mock_url_generator_builder)
167
167
 
168
- attachment_json = attachment.as_json
168
+ attachment.as_json
169
169
  assert mock_url_generator_builder.has_generated_url_with_style_name?('default style')
170
170
  end
171
171
 
@@ -346,11 +346,12 @@ class AttachmentTest < Test::Unit::TestCase
346
346
 
347
347
  context "An attachment with :hash interpolations" do
348
348
  setup do
349
- @file = StringIO.new("...")
349
+ @file = StringIO.new("...\n")
350
350
  end
351
351
 
352
352
  should "raise if no secret is provided" do
353
- @attachment = attachment :path => ":hash"
353
+ rebuild_model :path => ":hash"
354
+ @attachment = Dummy.new.avatar
354
355
  @attachment.assign @file
355
356
 
356
357
  assert_raise ArgumentError do
@@ -360,29 +361,23 @@ class AttachmentTest < Test::Unit::TestCase
360
361
 
361
362
  context "when secret is set" do
362
363
  setup do
363
- @attachment = attachment :path => ":hash", :hash_secret => "w00t"
364
- @attachment.stubs(:instance_read).with(:updated_at).returns(Time.at(1234567890))
365
- @attachment.stubs(:instance_read).with(:file_name).returns("bla.txt")
366
- @attachment.instance.id = 1234
364
+ rebuild_model :path => ":hash",
365
+ :hash_secret => "w00t",
366
+ :hash_data => ":class/:attachment/:style/:filename"
367
+ @attachment = Dummy.new.avatar
367
368
  @attachment.assign @file
368
369
  end
369
370
 
370
- should "interpolate the hash data" do
371
- @attachment.expects(:interpolate).with(@attachment.options[:hash_data],anything).returns("interpolated_stuff")
372
- @attachment.hash_key
373
- end
374
-
375
371
  should "result in the correct interpolation" do
376
- assert_equal "fake_models/avatars/1234/original/1234567890", @attachment.send(:interpolate,@attachment.options[:hash_data])
372
+ assert_equal "dummies/avatars/original/data.txt",
373
+ @attachment.send(:interpolate, @attachment.options[:hash_data])
374
+ assert_equal "dummies/avatars/thumb/data.txt",
375
+ @attachment.send(:interpolate, @attachment.options[:hash_data], :thumb)
377
376
  end
378
377
 
379
378
  should "result in a correct hash" do
380
- assert_equal "d22b617d1bf10016aa7d046d16427ae203f39fce", @attachment.path
381
- end
382
-
383
- should "generate a hash digest with the correct style" do
384
- OpenSSL::HMAC.expects(:hexdigest).with(anything, anything, "fake_models/avatars/1234/medium/1234567890")
385
- @attachment.path("medium")
379
+ assert_equal "e1079a5c34ddbd197ebd0280d07952d98a57fb30", @attachment.path
380
+ assert_equal "d740189bd3e49ef226fab84c8d45f7ae4126d043", @attachment.path(:thumb)
386
381
  end
387
382
  end
388
383
  end
@@ -406,15 +401,16 @@ class AttachmentTest < Test::Unit::TestCase
406
401
 
407
402
  context "An attachment with a default style and an extension interpolation" do
408
403
  setup do
409
- @attachment = attachment :path => ":basename.:extension",
410
- :styles => { :default => ["100x100", :png] },
411
- :default_style => :default
412
- @file = StringIO.new("...")
413
- @file.stubs(:original_filename).returns("file.jpg")
404
+ rebuild_model :path => ":basename.:extension",
405
+ :styles => { :default => ["100x100", :jpg] },
406
+ :default_style => :default
407
+ @attachment = Dummy.new.avatar
408
+ @file = File.open(fixture_file("5k.png"))
409
+ @file.stubs(:original_filename).returns("file.png")
414
410
  end
415
411
  should "return the right extension for the path" do
416
412
  @attachment.assign(@file)
417
- assert_equal "file.png", @attachment.path
413
+ assert_equal "file.jpg", @attachment.path
418
414
  end
419
415
  end
420
416
 
@@ -983,19 +979,16 @@ class AttachmentTest < Test::Unit::TestCase
983
979
  :path => ":rails_root/:attachment/:class/:style/:id/:basename.:extension"
984
980
  })
985
981
  FileUtils.rm_rf("tmp")
986
- rebuild_model
982
+ rebuild_model :styles => { :large => ["400x400", :jpg],
983
+ :medium => ["100x100", :jpg],
984
+ :small => ["32x32#", :jpg]},
985
+ :default_style => :small
987
986
  @instance = Dummy.new
988
987
  @instance.stubs(:id).returns 123
989
-
990
988
  @file = File.new(fixture_file("uppercase.PNG"), 'rb')
991
989
 
992
- styles = {:styles => { :large => ["400x400", :jpg],
993
- :medium => ["100x100", :jpg],
994
- :small => ["32x32#", :jpg]},
995
- :default_style => :small}
996
- @attachment = Paperclip::Attachment.new(:avatar,
997
- @instance,
998
- styles)
990
+ @attachment = @instance.avatar
991
+
999
992
  now = Time.now
1000
993
  Time.stubs(:now).returns(now)
1001
994
  @attachment.assign(@file)
@@ -1027,7 +1020,8 @@ class AttachmentTest < Test::Unit::TestCase
1027
1020
  rebuild_model
1028
1021
  @instance = Dummy.new
1029
1022
  @instance.stubs(:id).returns 123
1030
- @attachment = Paperclip::Attachment.new(:avatar, @instance)
1023
+ # @attachment = Paperclip::Attachment.new(:avatar, @instance)
1024
+ @attachment = @instance.avatar
1031
1025
  @file = File.new(fixture_file("5k.png"), 'rb')
1032
1026
  end
1033
1027
 
@@ -1045,7 +1039,7 @@ class AttachmentTest < Test::Unit::TestCase
1045
1039
 
1046
1040
  should 'clear out the previous assignment when assigned nil' do
1047
1041
  @attachment.assign(@file)
1048
- original_file = @attachment.queued_for_write[:original]
1042
+ @attachment.queued_for_write[:original]
1049
1043
  @attachment.assign(nil)
1050
1044
  assert_nil @attachment.queued_for_write[:original]
1051
1045
  end
@@ -1093,12 +1087,15 @@ class AttachmentTest < Test::Unit::TestCase
1093
1087
 
1094
1088
  context "when expecting three styles" do
1095
1089
  setup do
1096
- styles = {:styles => { :large => ["400x400", :png],
1097
- :medium => ["100x100", :gif],
1098
- :small => ["32x32#", :jpg]}}
1099
- @attachment = Paperclip::Attachment.new(:avatar,
1100
- @instance,
1101
- styles)
1090
+ rebuild_class :styles => {
1091
+ :large => ["400x400", :png],
1092
+ :medium => ["100x100", :gif],
1093
+ :small => ["32x32#", :jpg]
1094
+ }
1095
+ @instance = Dummy.new
1096
+ @instance.stubs(:id).returns 123
1097
+ @file = File.new(fixture_file("5k.png"), 'rb')
1098
+ @attachment = @instance.avatar
1102
1099
  end
1103
1100
 
1104
1101
  context "and assigned a file" do
@@ -1129,7 +1126,7 @@ class AttachmentTest < Test::Unit::TestCase
1129
1126
  [:small, 32, 32, "JPEG"]].each do |style|
1130
1127
  cmd = %Q[identify -format "%w %h %b %m" "#{@attachment.path(style.first)}"]
1131
1128
  out = `#{cmd}`
1132
- width, height, size, format = out.split(" ")
1129
+ width, height, _size, format = out.split(" ")
1133
1130
  assert_equal style[1].to_s, width.to_s
1134
1131
  assert_equal style[2].to_s, height.to_s
1135
1132
  assert_equal style[3].to_s, format.to_s
@@ -18,16 +18,6 @@ class ContentTypeDetectorTest < Test::Unit::TestCase
18
18
  assert_equal "video/mp4", Paperclip::ContentTypeDetector.new(@filename).detect
19
19
  end
20
20
 
21
- should 'find the first result that matches from the official types' do
22
- @filename = "/path/to/something.bmp"
23
- assert_equal "image/bmp", Paperclip::ContentTypeDetector.new(@filename).detect
24
- end
25
-
26
- should 'find the first unofficial result for this filename if no official ones exist' do
27
- @filename = "/path/to/something.aiff"
28
- assert_equal "audio/x-aiff", Paperclip::ContentTypeDetector.new(@filename).detect
29
- end
30
-
31
21
  should 'find the right type in the list via the file command' do
32
22
  @filename = "#{Dir.tmpdir}/something.hahalolnotreal"
33
23
  File.open(@filename, "w+") do |file|
@@ -0,0 +1 @@
1
+ <html></html>
@@ -119,7 +119,9 @@ class HasAttachedFileTest < Test::Unit::TestCase
119
119
  after_commit: nil,
120
120
  define_paperclip_callbacks: nil,
121
121
  extend: nil,
122
- name: 'Billy')
122
+ name: 'Billy',
123
+ validates_media_type_spoof_detection: nil
124
+ )
123
125
  end
124
126
  end
125
127
  end
@@ -40,6 +40,7 @@ class Test::Unit::TestCase
40
40
  Rails.stubs(:root).returns(Pathname.new(ROOT).join('tmp'))
41
41
  Rails.stubs(:env).returns('test')
42
42
  Rails.stubs(:const_defined?).with(:Railtie).returns(false)
43
+ ActiveSupport::Deprecation.silenced = true
43
44
  end
44
45
  end
45
46
 
@@ -125,6 +126,7 @@ end
125
126
  def rebuild_class options = {}
126
127
  reset_class("Dummy").tap do |klass|
127
128
  klass.has_attached_file :avatar, options
129
+ klass.do_not_validate_attachment_file_type :avatar
128
130
  Paperclip.reset_duplicate_clash_check!
129
131
  end
130
132
  end
@@ -132,6 +134,7 @@ end
132
134
  def rebuild_meta_class_of obj, options = {}
133
135
  (class << obj; self; end).tap do |metaklass|
134
136
  metaklass.has_attached_file :avatar, options
137
+ metaklass.do_not_validate_attachment_file_type :avatar
135
138
  Paperclip.reset_duplicate_clash_check!
136
139
  end
137
140
  end
@@ -169,21 +172,21 @@ end
169
172
 
170
173
  def should_accept_dummy_class
171
174
  should "accept the class" do
172
- assert_accepts @matcher, @dummy_class
175
+ assert_accepts @matcher, Dummy
173
176
  end
174
177
 
175
178
  should "accept an instance of that class" do
176
- assert_accepts @matcher, @dummy_class.new
179
+ assert_accepts @matcher, Dummy.new
177
180
  end
178
181
  end
179
182
 
180
183
  def should_reject_dummy_class
181
184
  should "reject the class" do
182
- assert_rejects @matcher, @dummy_class
185
+ assert_rejects @matcher, Dummy
183
186
  end
184
187
 
185
188
  should "reject an instance of that class" do
186
- assert_rejects @matcher, @dummy_class.new
189
+ assert_rejects @matcher, Dummy.new
187
190
  end
188
191
  end
189
192
 
@@ -197,6 +200,10 @@ def with_exitstatus_returning(code)
197
200
  end
198
201
  end
199
202
 
203
+ def stringy_file
204
+ StringIO.new('.\n')
205
+ end
206
+
200
207
  def fixture_file(filename)
201
208
  File.join(File.dirname(__FILE__), 'fixtures', filename)
202
209
  end