input_sanitizer 0.1.10 → 0.4.1

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/ci.yaml +26 -0
  3. data/.github/workflows/gempush.yml +28 -0
  4. data/.gitignore +2 -1
  5. data/CHANGELOG +99 -0
  6. data/LICENSE +201 -22
  7. data/README.md +24 -4
  8. data/input_sanitizer.gemspec +10 -4
  9. data/lib/input_sanitizer.rb +5 -2
  10. data/lib/input_sanitizer/errors.rb +142 -0
  11. data/lib/input_sanitizer/extended_converters.rb +5 -42
  12. data/lib/input_sanitizer/extended_converters/comma_joined_integers_converter.rb +15 -0
  13. data/lib/input_sanitizer/extended_converters/comma_joined_strings_converter.rb +15 -0
  14. data/lib/input_sanitizer/extended_converters/positive_integer_converter.rb +12 -0
  15. data/lib/input_sanitizer/extended_converters/specific_values_converter.rb +19 -0
  16. data/lib/input_sanitizer/restricted_hash.rb +49 -8
  17. data/lib/input_sanitizer/v1.rb +22 -0
  18. data/lib/input_sanitizer/v1/clean_field.rb +38 -0
  19. data/lib/input_sanitizer/{default_converters.rb → v1/default_converters.rb} +20 -13
  20. data/lib/input_sanitizer/v1/sanitizer.rb +166 -0
  21. data/lib/input_sanitizer/v2.rb +13 -0
  22. data/lib/input_sanitizer/v2/clean_field.rb +36 -0
  23. data/lib/input_sanitizer/v2/clean_payload_collection_field.rb +41 -0
  24. data/lib/input_sanitizer/v2/clean_query_collection_field.rb +40 -0
  25. data/lib/input_sanitizer/v2/error_collection.rb +49 -0
  26. data/lib/input_sanitizer/v2/nested_sanitizer_factory.rb +19 -0
  27. data/lib/input_sanitizer/v2/payload_sanitizer.rb +130 -0
  28. data/lib/input_sanitizer/v2/payload_transform.rb +42 -0
  29. data/lib/input_sanitizer/v2/query_sanitizer.rb +33 -0
  30. data/lib/input_sanitizer/v2/types.rb +227 -0
  31. data/lib/input_sanitizer/version.rb +1 -1
  32. data/spec/extended_converters/comma_joined_integers_converter_spec.rb +18 -0
  33. data/spec/extended_converters/comma_joined_strings_converter_spec.rb +18 -0
  34. data/spec/extended_converters/positive_integer_converter_spec.rb +18 -0
  35. data/spec/extended_converters/specific_values_converter_spec.rb +27 -0
  36. data/spec/restricted_hash_spec.rb +37 -7
  37. data/spec/sanitizer_spec.rb +129 -26
  38. data/spec/spec_helper.rb +17 -2
  39. data/spec/v1/default_converters_spec.rb +141 -0
  40. data/spec/v2/converters_spec.rb +174 -0
  41. data/spec/v2/payload_sanitizer_spec.rb +534 -0
  42. data/spec/v2/payload_transform_spec.rb +98 -0
  43. data/spec/v2/query_sanitizer_spec.rb +300 -0
  44. data/v2.md +52 -0
  45. metadata +105 -40
  46. data/.travis.yml +0 -12
  47. data/lib/input_sanitizer/sanitizer.rb +0 -140
  48. data/spec/default_converters_spec.rb +0 -101
  49. data/spec/extended_converters_spec.rb +0 -62
@@ -0,0 +1,42 @@
1
+ require 'active_support/core_ext/hash/indifferent_access'
2
+
3
+ class InputSanitizer::V2::PayloadTransform
4
+ attr_reader :original_payload, :context
5
+
6
+ def self.call(original_payload, context = {})
7
+ new(original_payload, context).call
8
+ end
9
+
10
+ def initialize(original_payload, context = {})
11
+ fail "#{self.class} is missing #transform method" unless respond_to?(:transform)
12
+ @original_payload, @context = original_payload, context
13
+ end
14
+
15
+ def call
16
+ transform
17
+ payload
18
+ end
19
+
20
+ private
21
+ def rename(from, to)
22
+ if has?(from)
23
+ data = payload.delete(from)
24
+ payload[to] = data
25
+ end
26
+ end
27
+
28
+ def merge_in(field, options = {})
29
+ if source = payload.delete(field)
30
+ source = options[:using].call(source) if options[:using]
31
+ payload.merge!(source)
32
+ end
33
+ end
34
+
35
+ def has?(key)
36
+ payload.has_key?(key)
37
+ end
38
+
39
+ def payload
40
+ @payload ||= original_payload.with_indifferent_access
41
+ end
42
+ end
@@ -0,0 +1,33 @@
1
+ class InputSanitizer::V2::QuerySanitizer < InputSanitizer::V2::PayloadSanitizer
2
+ def self.converters
3
+ {
4
+ :integer => InputSanitizer::V2::Types::CoercingIntegerCheck.new,
5
+ :float => InputSanitizer::V2::Types::CoercingFloatCheck.new,
6
+ :string => InputSanitizer::V2::Types::StringCheck.new,
7
+ :boolean => InputSanitizer::V2::Types::CoercingBooleanCheck.new,
8
+ :datetime => InputSanitizer::V2::Types::DatetimeCheck.new,
9
+ :date => InputSanitizer::V2::Types::DatetimeCheck.new(:check_date => true),
10
+ :url => InputSanitizer::V2::Types::URLCheck.new,
11
+ }
12
+ end
13
+ initialize_types_dsl
14
+
15
+ def self.sort_by(allowed_values, options = {})
16
+ set_keys_to_converter([:sort_by, { :allow => allowed_values }.merge(options)], InputSanitizer::V2::Types::SortByCheck.new)
17
+ end
18
+
19
+ # allow underscore cache buster by default
20
+ string :_
21
+
22
+ private
23
+ def perform_clean
24
+ super
25
+ @errors.each do |error|
26
+ error.field = error.field[1..-1] if error.field.start_with?('/')
27
+ end
28
+ end
29
+
30
+ def sanitizer_type
31
+ :query
32
+ end
33
+ end
@@ -0,0 +1,227 @@
1
+ require 'active_support/core_ext/object/blank'
2
+
3
+ module InputSanitizer::V2::Types
4
+ class IntegerCheck
5
+ def call(value, options = {})
6
+ if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
7
+ raise InputSanitizer::BlankValueError
8
+ elsif value == nil
9
+ value
10
+ else
11
+ Integer(value).tap do |integer|
12
+ raise InputSanitizer::TypeMismatchError.new(value, :integer) unless integer == value
13
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
14
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
15
+ end
16
+ end
17
+ rescue ArgumentError, TypeError
18
+ raise InputSanitizer::TypeMismatchError.new(value, :integer)
19
+ end
20
+ end
21
+
22
+ class CoercingIntegerCheck
23
+ def call(value, options = {})
24
+ if value == nil || value == 'null'
25
+ if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
26
+ raise InputSanitizer::BlankValueError
27
+ else
28
+ nil
29
+ end
30
+ else
31
+ Integer(value).tap do |integer|
32
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && integer < options[:minimum]
33
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && integer > options[:maximum]
34
+ end
35
+ end
36
+ rescue ArgumentError
37
+ raise InputSanitizer::TypeMismatchError.new(value, :integer)
38
+ end
39
+ end
40
+
41
+ class FloatCheck
42
+ def call(value, options = {})
43
+ if value == nil && (options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true)
44
+ raise InputSanitizer::BlankValueError
45
+ elsif value == nil
46
+ value
47
+ else
48
+ Float(value).tap do |float|
49
+ raise InputSanitizer::TypeMismatchError.new(value, :float) unless float == value
50
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
51
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
52
+ end
53
+ end
54
+ rescue ArgumentError, TypeError
55
+ raise InputSanitizer::TypeMismatchError.new(value, :float)
56
+ end
57
+ end
58
+
59
+ class CoercingFloatCheck
60
+ def call(value, options = {})
61
+ if value == nil || value == 'null'
62
+ if options[:allow_nil] == false || options[:allow_blank] == false || options[:required] == true
63
+ raise InputSanitizer::BlankValueError
64
+ else
65
+ nil
66
+ end
67
+ else
68
+ Float(value).tap do |float|
69
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && float < options[:minimum]
70
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && float > options[:maximum]
71
+ end
72
+ end
73
+ rescue ArgumentError
74
+ raise InputSanitizer::TypeMismatchError.new(value, :float)
75
+ end
76
+ end
77
+
78
+ class StringCheck
79
+ def call(value, options = {})
80
+ if options[:allow] && !options[:allow].include?(value)
81
+ raise InputSanitizer::ValueNotAllowedError.new(value)
82
+ elsif value.blank? && (options[:allow_blank] == false || options[:required] == true)
83
+ raise InputSanitizer::BlankValueError
84
+ elsif options[:regexp] && options[:regexp].match(value).nil?
85
+ raise InputSanitizer::RegexpMismatchError.new
86
+ elsif value == nil && options[:allow_nil] == false
87
+ raise InputSanitizer::BlankValueError
88
+ elsif value.blank?
89
+ value
90
+ else
91
+ value.to_s.tap do |string|
92
+ raise InputSanitizer::TypeMismatchError.new(value, :string) unless string == value
93
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && string.length < options[:minimum]
94
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && string.length > options[:maximum]
95
+ end
96
+
97
+ if options[:strip_4byte_chars] && !options[:already_stripped]
98
+ value_without_4byte_chars = strip_4byte_chars(value)
99
+ updated_options = options.merge(:already_stripped => true) # to prevent infinite loop
100
+ call(value_without_4byte_chars, updated_options) # run checks once again to ensure string is still valid after stripping 4-byte chars
101
+ else
102
+ value
103
+ end
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def strip_4byte_chars(string)
110
+ string.each_char.with_object(String.new) { |char, output| output << char if char.bytesize < 4 }
111
+ end
112
+ end
113
+
114
+ class BooleanCheck
115
+ def call(value, options = {})
116
+ if value == nil
117
+ raise InputSanitizer::BlankValueError
118
+ elsif [true, false].include?(value)
119
+ value
120
+ else
121
+ raise InputSanitizer::TypeMismatchError.new(value, :boolean)
122
+ end
123
+ end
124
+ end
125
+
126
+ class CoercingBooleanCheck
127
+ def call(value, options = {})
128
+ if [true, 'true'].include?(value)
129
+ true
130
+ elsif [false, 'false'].include?(value)
131
+ false
132
+ else
133
+ raise InputSanitizer::TypeMismatchError.new(value, :boolean)
134
+ end
135
+ end
136
+ end
137
+
138
+ class DatetimeCheck
139
+ def initialize(options = {})
140
+ @check_date = options && options[:check_date]
141
+ @klass = @check_date ? Date : DateTime
142
+ end
143
+
144
+ def call(value, options = {})
145
+ raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime) unless value == nil || value.is_a?(String)
146
+
147
+ if value.blank? && (options[:allow_blank] == false || options[:required] == true)
148
+ raise InputSanitizer::BlankValueError
149
+ elsif value == nil && options[:allow_nil] == false
150
+ raise InputSanitizer::BlankValueError
151
+ elsif value.blank?
152
+ value
153
+ else
154
+ @klass.parse(value).tap do |datetime|
155
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:minimum] && datetime < options[:minimum]
156
+ raise InputSanitizer::ValueError.new(value, options[:minimum], options[:maximum]) if options[:maximum] && datetime > options[:maximum]
157
+ end
158
+ end
159
+ rescue ArgumentError, TypeError
160
+ raise InputSanitizer::TypeMismatchError.new(value, @check_date ? :date : :datetime)
161
+ end
162
+ end
163
+
164
+ class URLCheck
165
+ def call(value, options = {})
166
+ if value.blank? && (options[:allow_blank] == false || options[:required] == true)
167
+ raise InputSanitizer::BlankValueError
168
+ elsif value == nil && options[:allow_nil] == false
169
+ raise InputSanitizer::BlankValueError
170
+ elsif value.blank?
171
+ value
172
+ else
173
+ unless /\A#{URI.regexp(%w(http https)).to_s}\z/.match(value)
174
+ raise InputSanitizer::TypeMismatchError.new(value, :url)
175
+ end
176
+ value
177
+ end
178
+ end
179
+ end
180
+
181
+ class SortByCheck
182
+ def call(value, options = {})
183
+ check_options!(options)
184
+
185
+ key, direction = split(value)
186
+ direction = 'asc' if direction.blank?
187
+
188
+ # special case when fallback takes care of separator sanitization e.g. custom fields
189
+ if options[:fallback] && !allowed_directions.include?(direction)
190
+ direction = 'asc'
191
+ key = value
192
+ end
193
+
194
+ unless valid?(key, direction, options)
195
+ raise InputSanitizer::ValueNotAllowedError.new(value)
196
+ end
197
+
198
+ [key, direction]
199
+ end
200
+
201
+ private
202
+ def valid?(key, direction, options)
203
+ allowed_keys = options[:allow]
204
+ fallback = options[:fallback]
205
+
206
+ allowed_directions.include?(direction) &&
207
+ ((allowed_keys && allowed_keys.include?(key)) ||
208
+ (fallback && fallback.call(key, direction, options)))
209
+ end
210
+
211
+ def split(value)
212
+ head, _, tail = value.to_s.rpartition(':')
213
+ head.empty? ? [tail, head] : [head, tail]
214
+ end
215
+
216
+ def check_options!(options)
217
+ fallback = options[:fallback]
218
+ if fallback && !fallback.respond_to?(:call)
219
+ raise ArgumentError, ":fallback option must respond to method :call (proc, lambda etc)"
220
+ end
221
+ end
222
+
223
+ def allowed_directions
224
+ ['asc', 'desc']
225
+ end
226
+ end
227
+ end
@@ -1,3 +1,3 @@
1
1
  module InputSanitizer
2
- VERSION = "0.1.10"
2
+ VERSION = "0.4.1"
3
3
  end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'input_sanitizer/extended_converters/comma_joined_integers_converter'
3
+
4
+ describe InputSanitizer::CommaJoinedIntegersConverter do
5
+ let(:converter) { described_class.new }
6
+
7
+ it "parses to array of ids" do
8
+ converter.call("1,2,3,5").should eq([1, 2, 3, 5])
9
+ end
10
+
11
+ it "converts to array if given an integer" do
12
+ converter.call(7).should eq([7])
13
+ end
14
+
15
+ it "raises on invalid character" do
16
+ lambda { converter.call(":") }.should raise_error(InputSanitizer::ConversionError)
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'input_sanitizer/extended_converters/comma_joined_strings_converter'
3
+
4
+ describe InputSanitizer::CommaJoinedStringsConverter do
5
+ let(:converter) { described_class.new }
6
+
7
+ it "parses to array of ids" do
8
+ converter.call("input,Sanitizer,ROCKS").should eq(["input", "Sanitizer", "ROCKS"])
9
+ end
10
+
11
+ it "allows underscores" do
12
+ converter.call("input_sanitizer,rocks").should eq(["input_sanitizer", "rocks"])
13
+ end
14
+
15
+ it "raises on invalid character" do
16
+ lambda { converter.call(":") }.should raise_error(InputSanitizer::ConversionError)
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'input_sanitizer/extended_converters/positive_integer_converter'
3
+
4
+ describe InputSanitizer::PositiveIntegerConverter do
5
+ let(:converter) { described_class.new }
6
+
7
+ it "casts string to integer" do
8
+ converter.call("3").should == 3
9
+ end
10
+
11
+ it "raises error if integer less than zero" do
12
+ lambda { converter.call("-3") }.should raise_error(InputSanitizer::ConversionError)
13
+ end
14
+
15
+ it "raises error if integer equals zero" do
16
+ lambda { converter.call("0") }.should raise_error(InputSanitizer::ConversionError)
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+ require 'input_sanitizer/extended_converters/specific_values_converter'
3
+
4
+ describe InputSanitizer::SpecificValuesConverter do
5
+ let(:converter) { described_class.new(values) }
6
+ let(:values) { [:a, :b] }
7
+
8
+ it "converts valid value to symbol" do
9
+ converter.call("b").should eq(:b)
10
+ end
11
+
12
+ it "raises on invalid value" do
13
+ lambda { converter.call("c") }.should raise_error(InputSanitizer::ConversionError)
14
+ end
15
+
16
+ it "raises on nil value" do
17
+ lambda { converter.call(nil) }.should raise_error(InputSanitizer::ConversionError)
18
+ end
19
+
20
+ context "when specific values are strings" do
21
+ let(:values) { ["a", "b"] }
22
+
23
+ it "keeps given value as string" do
24
+ converter.call("a").should eq("a")
25
+ end
26
+ end
27
+ end
@@ -1,19 +1,49 @@
1
- require "spec_helper"
1
+ require 'spec_helper'
2
2
 
3
3
  describe InputSanitizer::RestrictedHash do
4
- let(:hash) { InputSanitizer::RestrictedHash.new([:a, :b]) }
5
- subject { hash }
4
+ let(:hash) { InputSanitizer::RestrictedHash.new([:a]) }
6
5
 
7
- it "does not allow bad keys" do
8
- lambda{hash[:c]}.should raise_error(InputSanitizer::KeyNotAllowedError)
6
+ it 'does not allow bad keys' do
7
+ lambda{ hash[:b] }.should raise_error(InputSanitizer::KeyNotAllowedError)
9
8
  end
10
9
 
11
- it "does allow correct keys" do
10
+ it 'does allow correct keys' do
12
11
  hash[:a].should be_nil
13
12
  end
14
13
 
15
- it "returns value for correct key" do
14
+ it 'returns value for correct key' do
16
15
  hash[:a] = 'stuff'
17
16
  hash[:a].should == 'stuff'
18
17
  end
18
+
19
+ it 'allows to set value for a new key' do
20
+ hash[:b] = 'other stuff'
21
+ hash[:b].should == 'other stuff'
22
+ hash.key_allowed?(:b).should be_truthy
23
+ end
24
+
25
+ it 'adds new allowed keys after `transform_keys` is done' do
26
+ hash[:a] = 'stuff'
27
+ hash.transform_keys! { |key| key.to_s }
28
+ hash['a'].should == 'stuff'
29
+ hash.key_allowed?('a').should be_truthy
30
+ end
31
+
32
+ it 'removes previous allowed keys after `transform_keys` is done' do
33
+ hash[:a] = 'stuff'
34
+ hash.transform_keys! { |key| key.to_s }
35
+ lambda{ hash[:a] }.should raise_error(InputSanitizer::KeyNotAllowedError)
36
+ end
37
+
38
+ it 'updates allowed keys on merge!' do
39
+ merge_hash = { merged: '1' }
40
+ hash.merge!(merge_hash)
41
+ hash.key_allowed?(:merged).should be_truthy
42
+ end
43
+
44
+ it 'updates allowed keys on merge' do
45
+ merge_hash = { merged: '1' }
46
+ new_hash = hash.merge(some_key: merge_hash)
47
+ new_hash.key_allowed?(:some_key).should be_truthy
48
+ end
19
49
  end