jr-paperclip 7.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. checksums.yaml +7 -0
  2. data/.github/FUNDING.yml +3 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  4. data/.github/ISSUE_TEMPLATE/custom.md +10 -0
  5. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. data/.github/workflows/reviewdog.yml +23 -0
  7. data/.github/workflows/test.yml +46 -0
  8. data/.gitignore +19 -0
  9. data/.qlty/.gitignore +7 -0
  10. data/.qlty/qlty.toml +89 -0
  11. data/.rubocop.yml +1060 -0
  12. data/Appraisals +29 -0
  13. data/CONTRIBUTING.md +85 -0
  14. data/Gemfile +17 -0
  15. data/LICENSE +25 -0
  16. data/NEWS +567 -0
  17. data/README.md +1083 -0
  18. data/RELEASING.md +17 -0
  19. data/Rakefile +52 -0
  20. data/bin/console +11 -0
  21. data/features/basic_integration.feature +85 -0
  22. data/features/migration.feature +29 -0
  23. data/features/rake_tasks.feature +62 -0
  24. data/features/step_definitions/attachment_steps.rb +121 -0
  25. data/features/step_definitions/html_steps.rb +15 -0
  26. data/features/step_definitions/rails_steps.rb +271 -0
  27. data/features/step_definitions/s3_steps.rb +16 -0
  28. data/features/step_definitions/web_steps.rb +106 -0
  29. data/features/support/env.rb +12 -0
  30. data/features/support/file_helpers.rb +34 -0
  31. data/features/support/fixtures/boot_config.txt +15 -0
  32. data/features/support/fixtures/gemfile.txt +5 -0
  33. data/features/support/fixtures/preinitializer.txt +20 -0
  34. data/features/support/paths.rb +28 -0
  35. data/features/support/rails.rb +39 -0
  36. data/features/support/selectors.rb +19 -0
  37. data/features/support/webmock_setup.rb +8 -0
  38. data/gemfiles/7.0.gemfile +20 -0
  39. data/gemfiles/7.1.gemfile +20 -0
  40. data/gemfiles/7.2.gemfile +20 -0
  41. data/gemfiles/8.0.gemfile +20 -0
  42. data/gemfiles/8.1.gemfile +20 -0
  43. data/lib/generators/paperclip/USAGE +8 -0
  44. data/lib/generators/paperclip/paperclip_generator.rb +36 -0
  45. data/lib/generators/paperclip/templates/paperclip_migration.rb.erb +15 -0
  46. data/lib/jr-paperclip.rb +1 -0
  47. data/lib/paperclip/attachment.rb +634 -0
  48. data/lib/paperclip/attachment_registry.rb +60 -0
  49. data/lib/paperclip/callbacks.rb +42 -0
  50. data/lib/paperclip/content_type_detector.rb +85 -0
  51. data/lib/paperclip/errors.rb +34 -0
  52. data/lib/paperclip/file_command_content_type_detector.rb +28 -0
  53. data/lib/paperclip/filename_cleaner.rb +15 -0
  54. data/lib/paperclip/geometry.rb +157 -0
  55. data/lib/paperclip/geometry_detector_factory.rb +45 -0
  56. data/lib/paperclip/geometry_parser_factory.rb +31 -0
  57. data/lib/paperclip/glue.rb +18 -0
  58. data/lib/paperclip/has_attached_file.rb +116 -0
  59. data/lib/paperclip/helpers.rb +60 -0
  60. data/lib/paperclip/interpolations/plural_cache.rb +18 -0
  61. data/lib/paperclip/interpolations.rb +205 -0
  62. data/lib/paperclip/io_adapters/abstract_adapter.rb +75 -0
  63. data/lib/paperclip/io_adapters/attachment_adapter.rb +56 -0
  64. data/lib/paperclip/io_adapters/data_uri_adapter.rb +22 -0
  65. data/lib/paperclip/io_adapters/empty_string_adapter.rb +19 -0
  66. data/lib/paperclip/io_adapters/file_adapter.rb +26 -0
  67. data/lib/paperclip/io_adapters/http_url_proxy_adapter.rb +16 -0
  68. data/lib/paperclip/io_adapters/identity_adapter.rb +17 -0
  69. data/lib/paperclip/io_adapters/nil_adapter.rb +37 -0
  70. data/lib/paperclip/io_adapters/registry.rb +36 -0
  71. data/lib/paperclip/io_adapters/stringio_adapter.rb +36 -0
  72. data/lib/paperclip/io_adapters/uploaded_file_adapter.rb +44 -0
  73. data/lib/paperclip/io_adapters/uri_adapter.rb +78 -0
  74. data/lib/paperclip/locales/en.yml +18 -0
  75. data/lib/paperclip/locales/fr.yml +18 -0
  76. data/lib/paperclip/locales/gd.yml +20 -0
  77. data/lib/paperclip/logger.rb +21 -0
  78. data/lib/paperclip/matchers/have_attached_file_matcher.rb +54 -0
  79. data/lib/paperclip/matchers/validate_attachment_content_type_matcher.rb +101 -0
  80. data/lib/paperclip/matchers/validate_attachment_presence_matcher.rb +59 -0
  81. data/lib/paperclip/matchers/validate_attachment_size_matcher.rb +97 -0
  82. data/lib/paperclip/matchers.rb +64 -0
  83. data/lib/paperclip/media_type_spoof_detector.rb +93 -0
  84. data/lib/paperclip/missing_attachment_styles.rb +84 -0
  85. data/lib/paperclip/processor.rb +56 -0
  86. data/lib/paperclip/processor_helpers.rb +52 -0
  87. data/lib/paperclip/rails_environment.rb +21 -0
  88. data/lib/paperclip/railtie.rb +31 -0
  89. data/lib/paperclip/schema.rb +104 -0
  90. data/lib/paperclip/storage/filesystem.rb +99 -0
  91. data/lib/paperclip/storage/fog.rb +262 -0
  92. data/lib/paperclip/storage/s3.rb +497 -0
  93. data/lib/paperclip/storage.rb +3 -0
  94. data/lib/paperclip/style.rb +106 -0
  95. data/lib/paperclip/tempfile.rb +42 -0
  96. data/lib/paperclip/tempfile_factory.rb +22 -0
  97. data/lib/paperclip/thumbnail.rb +131 -0
  98. data/lib/paperclip/url_generator.rb +83 -0
  99. data/lib/paperclip/validators/attachment_content_type_validator.rb +95 -0
  100. data/lib/paperclip/validators/attachment_file_name_validator.rb +82 -0
  101. data/lib/paperclip/validators/attachment_file_type_ignorance_validator.rb +28 -0
  102. data/lib/paperclip/validators/attachment_presence_validator.rb +28 -0
  103. data/lib/paperclip/validators/attachment_size_validator.rb +126 -0
  104. data/lib/paperclip/validators/media_type_spoof_detection_validator.rb +29 -0
  105. data/lib/paperclip/validators.rb +73 -0
  106. data/lib/paperclip/version.rb +3 -0
  107. data/lib/paperclip.rb +215 -0
  108. data/lib/tasks/paperclip.rake +140 -0
  109. data/paperclip.gemspec +51 -0
  110. data/shoulda_macros/paperclip.rb +134 -0
  111. data/spec/database.yml +4 -0
  112. data/spec/paperclip/attachment_definitions_spec.rb +13 -0
  113. data/spec/paperclip/attachment_processing_spec.rb +79 -0
  114. data/spec/paperclip/attachment_registry_spec.rb +158 -0
  115. data/spec/paperclip/attachment_spec.rb +1617 -0
  116. data/spec/paperclip/content_type_detector_spec.rb +58 -0
  117. data/spec/paperclip/file_command_content_type_detector_spec.rb +40 -0
  118. data/spec/paperclip/filename_cleaner_spec.rb +13 -0
  119. data/spec/paperclip/geometry_detector_spec.rb +47 -0
  120. data/spec/paperclip/geometry_parser_spec.rb +73 -0
  121. data/spec/paperclip/geometry_spec.rb +267 -0
  122. data/spec/paperclip/glue_spec.rb +63 -0
  123. data/spec/paperclip/has_attached_file_spec.rb +78 -0
  124. data/spec/paperclip/integration_spec.rb +702 -0
  125. data/spec/paperclip/interpolations_spec.rb +270 -0
  126. data/spec/paperclip/io_adapters/abstract_adapter_spec.rb +160 -0
  127. data/spec/paperclip/io_adapters/attachment_adapter_spec.rb +167 -0
  128. data/spec/paperclip/io_adapters/data_uri_adapter_spec.rb +88 -0
  129. data/spec/paperclip/io_adapters/empty_string_adapter_spec.rb +17 -0
  130. data/spec/paperclip/io_adapters/file_adapter_spec.rb +134 -0
  131. data/spec/paperclip/io_adapters/http_url_proxy_adapter_spec.rb +142 -0
  132. data/spec/paperclip/io_adapters/identity_adapter_spec.rb +8 -0
  133. data/spec/paperclip/io_adapters/nil_adapter_spec.rb +25 -0
  134. data/spec/paperclip/io_adapters/registry_spec.rb +35 -0
  135. data/spec/paperclip/io_adapters/stringio_adapter_spec.rb +64 -0
  136. data/spec/paperclip/io_adapters/uploaded_file_adapter_spec.rb +146 -0
  137. data/spec/paperclip/io_adapters/uri_adapter_spec.rb +231 -0
  138. data/spec/paperclip/matchers/have_attached_file_matcher_spec.rb +19 -0
  139. data/spec/paperclip/matchers/validate_attachment_content_type_matcher_spec.rb +108 -0
  140. data/spec/paperclip/matchers/validate_attachment_presence_matcher_spec.rb +69 -0
  141. data/spec/paperclip/matchers/validate_attachment_size_matcher_spec.rb +88 -0
  142. data/spec/paperclip/media_type_spoof_detector_spec.rb +126 -0
  143. data/spec/paperclip/meta_class_spec.rb +30 -0
  144. data/spec/paperclip/paperclip_missing_attachment_styles_spec.rb +88 -0
  145. data/spec/paperclip/paperclip_spec.rb +196 -0
  146. data/spec/paperclip/plural_cache_spec.rb +37 -0
  147. data/spec/paperclip/processor_helpers_spec.rb +57 -0
  148. data/spec/paperclip/processor_spec.rb +26 -0
  149. data/spec/paperclip/rails_environment_spec.rb +30 -0
  150. data/spec/paperclip/rake_spec.rb +103 -0
  151. data/spec/paperclip/schema_spec.rb +298 -0
  152. data/spec/paperclip/storage/filesystem_spec.rb +102 -0
  153. data/spec/paperclip/storage/fog_spec.rb +606 -0
  154. data/spec/paperclip/storage/s3_live_spec.rb +188 -0
  155. data/spec/paperclip/storage/s3_spec.rb +1974 -0
  156. data/spec/paperclip/style_spec.rb +251 -0
  157. data/spec/paperclip/tempfile_factory_spec.rb +33 -0
  158. data/spec/paperclip/tempfile_spec.rb +35 -0
  159. data/spec/paperclip/thumbnail_spec.rb +504 -0
  160. data/spec/paperclip/url_generator_spec.rb +231 -0
  161. data/spec/paperclip/validators/attachment_content_type_validator_spec.rb +410 -0
  162. data/spec/paperclip/validators/attachment_file_name_validator_spec.rb +249 -0
  163. data/spec/paperclip/validators/attachment_presence_validator_spec.rb +85 -0
  164. data/spec/paperclip/validators/attachment_size_validator_spec.rb +325 -0
  165. data/spec/paperclip/validators/media_type_spoof_detection_validator_spec.rb +48 -0
  166. data/spec/paperclip/validators_spec.rb +179 -0
  167. data/spec/spec_helper.rb +52 -0
  168. data/spec/support/assertions.rb +84 -0
  169. data/spec/support/fake_model.rb +24 -0
  170. data/spec/support/fake_rails.rb +12 -0
  171. data/spec/support/fixtures/12k.png +0 -0
  172. data/spec/support/fixtures/50x50.png +0 -0
  173. data/spec/support/fixtures/5k.png +0 -0
  174. data/spec/support/fixtures/animated +0 -0
  175. data/spec/support/fixtures/animated.gif +0 -0
  176. data/spec/support/fixtures/animated.unknown +0 -0
  177. data/spec/support/fixtures/aws_s3.yml +13 -0
  178. data/spec/support/fixtures/bad.png +1 -0
  179. data/spec/support/fixtures/empty.html +1 -0
  180. data/spec/support/fixtures/empty.xlsx +0 -0
  181. data/spec/support/fixtures/fog.yml +8 -0
  182. data/spec/support/fixtures/rotated.jpg +0 -0
  183. data/spec/support/fixtures/s3.yml +8 -0
  184. data/spec/support/fixtures/sample.xlsm +0 -0
  185. data/spec/support/fixtures/spaced file.jpg +0 -0
  186. data/spec/support/fixtures/spaced file.png +0 -0
  187. data/spec/support/fixtures/text.txt +1 -0
  188. data/spec/support/fixtures/twopage.pdf +0 -0
  189. data/spec/support/fixtures/uppercase.PNG +0 -0
  190. data/spec/support/matchers/accept.rb +5 -0
  191. data/spec/support/matchers/exist.rb +5 -0
  192. data/spec/support/matchers/have_column.rb +23 -0
  193. data/spec/support/mock_attachment.rb +24 -0
  194. data/spec/support/mock_interpolator.rb +24 -0
  195. data/spec/support/mock_url_generator_builder.rb +26 -0
  196. data/spec/support/model_reconstruction.rb +72 -0
  197. data/spec/support/reporting.rb +11 -0
  198. data/spec/support/test_data.rb +13 -0
  199. data/spec/support/version_helper.rb +9 -0
  200. metadata +702 -0
@@ -0,0 +1,131 @@
1
+ module Paperclip
2
+ # Handles thumbnailing images that are uploaded.
3
+ class Thumbnail < Processor
4
+ attr_accessor :current_geometry, :target_geometry, :format, :whiny, :convert_options,
5
+ :source_file_options, :animated, :auto_orient, :frame_index
6
+
7
+ # List of formats that we need to preserve animation
8
+ ANIMATED_FORMATS = %w(gif).freeze
9
+ MULTI_FRAME_FORMATS = %w(.mkv .avi .mp4 .mov .mpg .mpeg .gif).freeze
10
+
11
+ # Creates a Thumbnail object set to work on the +file+ given. It
12
+ # will attempt to transform the image into one defined by +target_geometry+
13
+ # which is a "WxH"-style string. +format+ will be inferred from the +file+
14
+ # unless specified. Thumbnail creation will raise no errors unless
15
+ # +whiny+ is true (which it is, by default. If +convert_options+ is
16
+ # set, the options will be appended to the convert command upon image conversion
17
+ #
18
+ # Options include:
19
+ #
20
+ # +geometry+ - the desired width and height of the thumbnail (required)
21
+ # +file_geometry_parser+ - an object with a method named +from_file+ that takes an image file and produces its geometry and a +transformation_to+. Defaults to Paperclip::Geometry
22
+ # +string_geometry_parser+ - an object with a method named +parse+ that takes a string and produces an object with +width+, +height+, and +to_s+ accessors. Defaults to Paperclip::Geometry
23
+ # +source_file_options+ - flags passed to the +convert+ command that influence how the source file is read
24
+ # +convert_options+ - flags passed to the +convert+ command that influence how the image is processed
25
+ # +whiny+ - whether to raise an error when processing fails. Defaults to true
26
+ # +format+ - the desired filename extension
27
+ # +animated+ - whether to merge all the layers in the image. Defaults to true
28
+ # +frame_index+ - the frame index of the source file to render as the thumbnail
29
+ def initialize(file, options = {}, attachment = nil)
30
+ super
31
+
32
+ geometry = options[:geometry].to_s
33
+ @crop = geometry[-1, 1] == "#"
34
+ @target_geometry = options.fetch(:string_geometry_parser, Geometry).parse(geometry)
35
+ @current_geometry = options.fetch(:file_geometry_parser, Geometry).from_file(@file)
36
+ @source_file_options = options[:source_file_options]
37
+ @convert_options = options[:convert_options]
38
+ @whiny = options.fetch(:whiny, true)
39
+ @format = options[:format]
40
+ @animated = options.fetch(:animated, true)
41
+ @auto_orient = options.fetch(:auto_orient, true)
42
+ @current_geometry.auto_orient if @auto_orient && @current_geometry.respond_to?(:auto_orient)
43
+ @source_file_options = @source_file_options.split(/\s+/) if @source_file_options.respond_to?(:split)
44
+ @convert_options = @convert_options.split(/\s+/) if @convert_options.respond_to?(:split)
45
+
46
+ @current_format = File.extname(@file.path)
47
+ @basename = File.basename(@file.path, @current_format)
48
+ @frame_index = multi_frame_format? ? options.fetch(:frame_index, 0) : 0
49
+ end
50
+
51
+ # Returns true if the +target_geometry+ is meant to crop.
52
+ def crop?
53
+ @crop
54
+ end
55
+
56
+ # Returns true if the image is meant to make use of additional convert options.
57
+ def convert_options?
58
+ !@convert_options.nil? && !@convert_options.empty?
59
+ end
60
+
61
+ # Performs the conversion of the +file+ into a thumbnail. Returns the Tempfile
62
+ # that contains the new image.
63
+ def make
64
+ src = @file
65
+ filename = [@basename, @format ? ".#{@format}" : ""].join
66
+ dst = TempfileFactory.new.generate(filename)
67
+
68
+ begin
69
+ parameters = []
70
+ parameters << source_file_options
71
+ parameters << ":source"
72
+ parameters << transformation_command
73
+ parameters << convert_options
74
+ parameters << ":dest"
75
+
76
+ parameters = parameters.flatten.compact.join(" ").strip.squeeze(" ")
77
+
78
+ frame = animated? ? "" : "[#{@frame_index}]"
79
+ convert(
80
+ parameters,
81
+ source: "#{File.expand_path(src.path)}#{frame}",
82
+ dest: File.expand_path(dst.path)
83
+ )
84
+ rescue Terrapin::ExitStatusError => e
85
+ if @whiny
86
+ message = "There was an error processing the thumbnail for #{@basename}:\n" + e.message
87
+ raise Paperclip::Error, message
88
+ end
89
+ rescue Terrapin::CommandNotFoundError => e
90
+ raise Paperclip::Errors::CommandNotFoundError.new("Could not run the `convert` command. Please install ImageMagick.")
91
+ end
92
+
93
+ dst
94
+ end
95
+
96
+ # Returns the command ImageMagick's +convert+ needs to transform the image
97
+ # into the thumbnail.
98
+ def transformation_command
99
+ scale, crop = @current_geometry.transformation_to(@target_geometry, crop?)
100
+ trans = []
101
+ trans << "-coalesce" if animated?
102
+ trans << "-auto-orient" if auto_orient
103
+ trans << "-resize" << %["#{scale}"] unless scale.nil? || scale.empty?
104
+ trans << "-crop" << %["#{crop}"] << "+repage" if crop
105
+ trans << '-layers "optimize"' if animated?
106
+ trans
107
+ end
108
+
109
+ protected
110
+
111
+ def multi_frame_format?
112
+ MULTI_FRAME_FORMATS.include? @current_format
113
+ end
114
+
115
+ def animated?
116
+ @animated && (ANIMATED_FORMATS.include?(@format.to_s) || @format.blank?) && identified_as_animated?
117
+ end
118
+
119
+ # Return true if ImageMagick's +identify+ returns an animated format
120
+ def identified_as_animated?
121
+ if @identified_as_animated.nil?
122
+ @identified_as_animated = ANIMATED_FORMATS.include? identify("-format %m :file", file: "#{@file.path}[0]").to_s.downcase.strip
123
+ end
124
+ @identified_as_animated
125
+ rescue Terrapin::ExitStatusError => e
126
+ raise Paperclip::Error, "There was an error running `identify` for #{@basename}" if @whiny
127
+ rescue Terrapin::CommandNotFoundError => e
128
+ raise Paperclip::Errors::CommandNotFoundError.new("Could not run the `identify` command. Please install ImageMagick.")
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,83 @@
1
+ require "uri"
2
+ require "active_support/core_ext/module/delegation"
3
+
4
+ module Paperclip
5
+ class UrlGenerator
6
+ class << self
7
+ def encoder
8
+ @encoder ||= URI::RFC2396_Parser.new
9
+ end
10
+ delegate :escape, :unescape, to: :encoder
11
+ end
12
+
13
+ def initialize(attachment)
14
+ @attachment = attachment
15
+ end
16
+
17
+ def for(style_name, options)
18
+ interpolated = attachment_options[:interpolator].interpolate(
19
+ most_appropriate_url, @attachment, style_name
20
+ )
21
+
22
+ escaped = escape_url_as_needed(interpolated, options)
23
+ timestamp_as_needed(escaped, options)
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :attachment
29
+ delegate :options, to: :attachment, prefix: true
30
+
31
+ # This method is all over the place.
32
+ def default_url
33
+ if attachment_options[:default_url].respond_to?(:call)
34
+ attachment_options[:default_url].call(@attachment)
35
+ elsif attachment_options[:default_url].is_a?(Symbol)
36
+ @attachment.instance.send(attachment_options[:default_url])
37
+ else
38
+ attachment_options[:default_url]
39
+ end
40
+ end
41
+
42
+ def most_appropriate_url
43
+ if @attachment.original_filename.nil?
44
+ default_url
45
+ else
46
+ attachment_options[:url]
47
+ end
48
+ end
49
+
50
+ def timestamp_as_needed(url, options)
51
+ if options[:timestamp] && timestamp_possible?
52
+ delimiter_char = url.match(/\?.+=/) ? "&" : "?"
53
+ "#{url}#{delimiter_char}#{@attachment.updated_at}"
54
+ else
55
+ url
56
+ end
57
+ end
58
+
59
+ def timestamp_possible?
60
+ @attachment.respond_to?(:updated_at) && @attachment.updated_at.present?
61
+ end
62
+
63
+ def escape_url_as_needed(url, options)
64
+ if options[:escape]
65
+ escape_url(url)
66
+ else
67
+ url
68
+ end
69
+ end
70
+
71
+ def escape_url(url)
72
+ if url.respond_to?(:escape)
73
+ url.escape
74
+ else
75
+ self.class.escape(url).gsub(escape_regex) { |m| "%#{m.ord.to_s(16).upcase}" }
76
+ end
77
+ end
78
+
79
+ def escape_regex
80
+ /[\?\(\)\[\]\+]/
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,95 @@
1
+ module Paperclip
2
+ module Validators
3
+ class AttachmentContentTypeValidator < ActiveModel::EachValidator
4
+ def initialize(options)
5
+ options[:allow_nil] = true unless options.key?(:allow_nil)
6
+ unless options.key?(:add_validation_errors_to)
7
+ options[:add_validation_errors_to] = Paperclip.options[:add_validation_errors_to]
8
+ end
9
+ super
10
+ end
11
+
12
+ def self.helper_method_name
13
+ :validates_attachment_content_type
14
+ end
15
+
16
+ def validate_each(record, attribute, value)
17
+ base_attribute = attribute.to_sym
18
+ attribute = "#{attribute}_content_type".to_sym
19
+ value = record.send :read_attribute_for_validation, attribute
20
+
21
+ return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
22
+
23
+ validate_whitelist(record, attribute, value)
24
+ validate_blacklist(record, attribute, value)
25
+
26
+ if record.errors.include?(attribute) &&
27
+ [:both, :base].include?(options[:add_validation_errors_to])
28
+
29
+ record.errors[attribute].each do |error|
30
+ record.errors.add base_attribute, error
31
+ end
32
+
33
+ record.errors.delete(attribute) if options[:add_validation_errors_to] == :base
34
+ end
35
+ end
36
+
37
+ def validate_whitelist(record, attribute, value)
38
+ if allowed_types.present? && allowed_types.none? { |type| type === value }
39
+ mark_invalid record, attribute, allowed_types
40
+ end
41
+ end
42
+
43
+ def validate_blacklist(record, attribute, value)
44
+ if forbidden_types.present? && forbidden_types.any? { |type| type === value }
45
+ mark_invalid record, attribute, forbidden_types
46
+ end
47
+ end
48
+
49
+ def mark_invalid(record, attribute, types)
50
+ record.errors.add attribute, :invalid, **options.merge(types: types.join(", "))
51
+ end
52
+
53
+ def allowed_types
54
+ [options[:content_type]].flatten.compact
55
+ end
56
+
57
+ def forbidden_types
58
+ [options[:not]].flatten.compact
59
+ end
60
+
61
+ def check_validity!
62
+ unless options.key?(:content_type) || options.key?(:not)
63
+ raise ArgumentError, "You must pass in either :content_type or :not to the validator"
64
+ end
65
+ end
66
+ end
67
+
68
+ module HelperMethods
69
+ # Places ActiveModel validations on the content type of the file
70
+ # assigned. The possible options are:
71
+ # * +content_type+: Allowed content types. Can be a single content type
72
+ # or an array. Each type can be a String or a Regexp. It should be
73
+ # noted that Internet Explorer uploads files with content_types that you
74
+ # may not expect. For example, JPEG images are given image/pjpeg and
75
+ # PNGs are image/x-png, so keep that in mind when determining how you
76
+ # match. Allows all by default.
77
+ # * +not+: Forbidden content types.
78
+ # * +message+: The message to display when the uploaded file has an invalid
79
+ # content type.
80
+ # * +if+: A lambda or name of an instance method. Validation will only
81
+ # be run is this lambda or method returns true.
82
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
83
+ # NOTE: If you do not specify an [attachment]_content_type field on your
84
+ # model, content_type validation will work _ONLY upon assignment_ and
85
+ # re-validation after the instance has been reloaded will always succeed.
86
+ # You'll still need to have a virtual attribute (created by +attr_accessor+)
87
+ # name +[attachment]_content_type+ to be able to use this validator.
88
+ def validates_attachment_content_type(*attr_names)
89
+ options = _merge_attributes(attr_names)
90
+ validates_with AttachmentContentTypeValidator, options.dup
91
+ validate_before_processing AttachmentContentTypeValidator, options.dup
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,82 @@
1
+ module Paperclip
2
+ module Validators
3
+ class AttachmentFileNameValidator < ActiveModel::EachValidator
4
+ def initialize(options)
5
+ options[:allow_nil] = true unless options.key?(:allow_nil)
6
+ unless options.key?(:add_validation_errors_to)
7
+ options[:add_validation_errors_to] = Paperclip.options[:add_validation_errors_to]
8
+ end
9
+ super
10
+ end
11
+
12
+ def self.helper_method_name
13
+ :validates_attachment_file_name
14
+ end
15
+
16
+ def validate_each(record, attribute, value)
17
+ base_attribute = attribute.to_sym
18
+ attribute = "#{attribute}_file_name".to_sym
19
+ value = record.send :read_attribute_for_validation, attribute
20
+
21
+ return if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
22
+
23
+ validate_whitelist(record, attribute, value)
24
+ validate_blacklist(record, attribute, value)
25
+
26
+ if record.errors.include?(attribute) &&
27
+ [:both, :base].include?(options[:add_validation_errors_to])
28
+
29
+ record.errors[attribute].each do |error|
30
+ record.errors.add(base_attribute, error)
31
+ end
32
+
33
+ record.errors.delete(attribute) if options[:add_validation_errors_to] == :base
34
+ end
35
+ end
36
+
37
+ def validate_whitelist(record, attribute, value)
38
+ mark_invalid record, attribute, allowed if allowed.present? && allowed.none? { |type| type === value }
39
+ end
40
+
41
+ def validate_blacklist(record, attribute, value)
42
+ mark_invalid record, attribute, forbidden if forbidden.present? && forbidden.any? { |type| type === value }
43
+ end
44
+
45
+ def mark_invalid(record, attribute, patterns)
46
+ record.errors.add attribute, :invalid, **options.merge(names: patterns.join(", "))
47
+ end
48
+
49
+ def allowed
50
+ [options[:matches]].flatten.compact
51
+ end
52
+
53
+ def forbidden
54
+ [options[:not]].flatten.compact
55
+ end
56
+
57
+ def check_validity!
58
+ unless options.key?(:matches) || options.key?(:not)
59
+ raise ArgumentError, "You must pass in either :matches or :not to the validator"
60
+ end
61
+ end
62
+ end
63
+
64
+ module HelperMethods
65
+ # Places ActiveModel validations on the name of the file
66
+ # assigned. The possible options are:
67
+ # * +matches+: Allowed filename patterns as Regexps. Can be a single one
68
+ # or an array.
69
+ # * +not+: Forbidden file name patterns, specified the same was as +matches+.
70
+ # * +message+: The message to display when the uploaded file has an invalid
71
+ # name.
72
+ # * +if+: A lambda or name of an instance method. Validation will only
73
+ # be run is this lambda or method returns true.
74
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
75
+ def validates_attachment_file_name(*attr_names)
76
+ options = _merge_attributes(attr_names)
77
+ validates_with AttachmentFileNameValidator, options.dup
78
+ validate_before_processing AttachmentFileNameValidator, options.dup
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,28 @@
1
+ require "active_model/validations/presence"
2
+
3
+ module Paperclip
4
+ module Validators
5
+ class AttachmentPresenceValidator < ActiveModel::EachValidator
6
+ def validate_each(record, attribute, _value)
7
+ record.errors.add(attribute, :blank, **options) if record.send("#{attribute}_file_name").blank?
8
+ end
9
+
10
+ def self.helper_method_name
11
+ :validates_attachment_presence
12
+ end
13
+ end
14
+
15
+ module HelperMethods
16
+ # Places ActiveModel validations on the presence of a file.
17
+ # Options:
18
+ # * +if+: A lambda or name of an instance method. Validation will only
19
+ # be run if this lambda or method returns true.
20
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
21
+ def validates_attachment_presence(*attr_names)
22
+ options = _merge_attributes(attr_names)
23
+ validates_with AttachmentPresenceValidator, options.dup
24
+ validate_before_processing AttachmentPresenceValidator, options.dup
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,126 @@
1
+ require "active_model/validations/numericality"
2
+
3
+ module Paperclip
4
+ module Validators
5
+ class AttachmentSizeValidator < ActiveModel::Validations::NumericalityValidator
6
+ AVAILABLE_CHECKS = [:less_than, :less_than_or_equal_to, :greater_than, :greater_than_or_equal_to].freeze
7
+
8
+ def initialize(options)
9
+ extract_options(options)
10
+ super
11
+ end
12
+
13
+ def self.helper_method_name
14
+ :validates_attachment_size
15
+ end
16
+
17
+ def validate_each(record, attr_name, value)
18
+ base_attr_name = attr_name
19
+ attr_name = "#{attr_name}_file_size".to_sym
20
+
21
+ error_attrs = []
22
+ case options[:add_validation_errors_to]
23
+ when :base
24
+ error_attrs << base_attr_name
25
+ when :attribute
26
+ error_attrs << attr_name
27
+ else
28
+ error_attrs << base_attr_name
29
+ error_attrs << attr_name
30
+ end
31
+
32
+ value = record.send(:read_attribute_for_validation, attr_name)
33
+
34
+ unless value.blank?
35
+ options.slice(*AVAILABLE_CHECKS).each do |option, option_value|
36
+ option_value = option_value.call(record) if option_value.is_a?(Proc)
37
+ option_value = extract_option_value(option, option_value)
38
+ operator = ActiveModel::VERSION::MAJOR >= 7 ? COMPARE_CHECKS[option] : CHECKS[option]
39
+
40
+ unless value.send(operator, option_value)
41
+ error_message_key = options[:in] ? :in_between : option
42
+ error_attrs.each do |error_attr_name|
43
+ record.errors.add(error_attr_name, error_message_key, **filtered_options(value).merge(
44
+ min: min_value_in_human_size(record),
45
+ max: max_value_in_human_size(record),
46
+ count: human_size(option_value)
47
+ ))
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ def check_validity!
55
+ unless (AVAILABLE_CHECKS + [:in]).any? { |argument| options.key?(argument) }
56
+ raise ArgumentError, "You must pass either :less_than, :greater_than, or :in to the validator"
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ def extract_options(options)
63
+ if range = options[:in]
64
+ if !options[:in].respond_to?(:call)
65
+ options[:less_than_or_equal_to] = range.max
66
+ options[:greater_than_or_equal_to] = range.min
67
+ else
68
+ options[:less_than_or_equal_to] = range
69
+ options[:greater_than_or_equal_to] = range
70
+ end
71
+ end
72
+
73
+ unless options.key?(:add_validation_errors_to)
74
+ options[:add_validation_errors_to] = Paperclip.options[:add_validation_errors_to]
75
+ end
76
+ end
77
+
78
+ def extract_option_value(option, option_value)
79
+ if option_value.is_a?(Range)
80
+ if [:less_than, :less_than_or_equal_to].include?(option)
81
+ option_value.max
82
+ else
83
+ option_value.min
84
+ end
85
+ else
86
+ option_value
87
+ end
88
+ end
89
+
90
+ def human_size(size)
91
+ ActiveSupport::NumberHelper.number_to_human_size(size)
92
+ end
93
+
94
+ def min_value_in_human_size(record)
95
+ value = options[:greater_than_or_equal_to] || options[:greater_than]
96
+ value = value.call(record) if value.respond_to?(:call)
97
+ value = value.min if value.respond_to?(:min)
98
+ human_size(value)
99
+ end
100
+
101
+ def max_value_in_human_size(record)
102
+ value = options[:less_than_or_equal_to] || options[:less_than]
103
+ value = value.call(record) if value.respond_to?(:call)
104
+ value = value.max if value.respond_to?(:max)
105
+ human_size(value)
106
+ end
107
+ end
108
+
109
+ module HelperMethods
110
+ # Places ActiveModel validations on the size of the file assigned. The
111
+ # possible options are:
112
+ # * +in+: a Range of bytes (i.e. +1..1.megabyte+),
113
+ # * +less_than+: equivalent to :in => 0..options[:less_than]
114
+ # * +greater_than+: equivalent to :in => options[:greater_than]..Infinity
115
+ # * +message+: error message to display, use :min and :max as replacements
116
+ # * +if+: A lambda or name of an instance method. Validation will only
117
+ # be run if this lambda or method returns true.
118
+ # * +unless+: Same as +if+ but validates if lambda or method returns false.
119
+ def validates_attachment_size(*attr_names)
120
+ options = _merge_attributes(attr_names)
121
+ validates_with AttachmentSizeValidator, options.dup
122
+ validate_before_processing AttachmentSizeValidator, options.dup
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,29 @@
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, value.content_type).spoofed?
9
+ record.errors.add(attribute, :spoofed_media_type)
10
+ end
11
+
12
+ adapter.tempfile.close(true) if adapter.tempfile
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 validates_media_type_spoof_detection(*attr_names)
23
+ options = _merge_attributes(attr_names)
24
+ validates_with MediaTypeSpoofDetectionValidator, options.dup
25
+ validate_before_processing MediaTypeSpoofDetectionValidator, options.dup
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,73 @@
1
+ require "active_model"
2
+ require "active_support/concern"
3
+ require "active_support/core_ext/array/wrap"
4
+ require "paperclip/validators/attachment_content_type_validator"
5
+ require "paperclip/validators/attachment_file_name_validator"
6
+ require "paperclip/validators/attachment_presence_validator"
7
+ require "paperclip/validators/attachment_size_validator"
8
+ require "paperclip/validators/media_type_spoof_detection_validator"
9
+ require "paperclip/validators/attachment_file_type_ignorance_validator"
10
+
11
+ module Paperclip
12
+ module Validators
13
+ extend ActiveSupport::Concern
14
+
15
+ included do
16
+ extend HelperMethods
17
+ include HelperMethods
18
+ end
19
+
20
+ ::Paperclip::REQUIRED_VALIDATORS = [AttachmentFileNameValidator, AttachmentContentTypeValidator, AttachmentFileTypeIgnoranceValidator].freeze
21
+
22
+ module ClassMethods
23
+ # This method is a shortcut to the validator classes that are in
24
+ # "Attachment...Validator" format. It is almost the same as the
25
+ # +validates+ method that ships with Rails, but is customized for
26
+ # use with attachment validators. This is helpful when you're using
27
+ # multiple attachment validators on a single attachment.
28
+ #
29
+ # Example of using the validator:
30
+ #
31
+ # validates_attachment :avatar, :presence => true,
32
+ # :content_type => { :content_type => "image/jpg" },
33
+ # :size => { :in => 0..10.kilobytes }
34
+ #
35
+ def validates_attachment(*attributes)
36
+ options = attributes.extract_options!.dup
37
+
38
+ Paperclip::Validators.constants.each do |constant|
39
+ if constant.to_s =~ /\AAttachment(.+)Validator\z/
40
+ validator_kind = $1.underscore.to_sym
41
+
42
+ if options.key?(validator_kind)
43
+ validator_options = options.delete(validator_kind)
44
+ validator_options = {} if validator_options == true
45
+ conditional_options = options.slice(:if, :unless)
46
+ Array.wrap(validator_options).each do |local_options|
47
+ method_name = Paperclip::Validators.const_get(constant.to_s).helper_method_name
48
+ send(method_name, attributes, local_options.merge(conditional_options))
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ def validate_before_processing(validator_class, options)
56
+ options = options.dup
57
+ attributes = options.delete(:attributes)
58
+ attributes.each do |attribute|
59
+ options[:attributes] = [attribute]
60
+ create_validating_before_filter(attribute, validator_class, options)
61
+ end
62
+ end
63
+
64
+ def create_validating_before_filter(attribute, validator_class, options)
65
+ if_clause = options.delete(:if)
66
+ unless_clause = options.delete(:unless)
67
+ send(:"before_#{attribute}_validate", if: if_clause, unless: unless_clause) do |*_args|
68
+ validator_class.new(options.dup).validate(self)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,3 @@
1
+ module Paperclip
2
+ VERSION = "7.3.0" unless defined?(Paperclip::VERSION)
3
+ end