Skip to content

Commit fccf89f

Browse files
wadetandyJack Casey
authored andcommitted
Properly excape prefix and suffix queries
In #196 I added sanitization for `match` filters but I negelected to handle `prefix` and `suffix` filters, which share similar logic. This handles all three. Additionally, issue #207 indicates an issue with the `sanitize_sql_like` method not always being available on the scope object. Wasn't able to reproduce myself, but this switches to avoid using the method from scope directly and instead creates a utility class which is extended with the appropriate sanitization methods.
1 parent 3416a32 commit fccf89f

File tree

2 files changed

+90
-29
lines changed

2 files changed

+90
-29
lines changed

lib/graphiti/adapters/active_record.rb

Lines changed: 54 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -57,41 +57,28 @@ def filter_string_not_eql(scope, attribute, value)
5757
filter_string_eql(scope, attribute, value, is_not: true)
5858
end
5959

60-
def filter_string_prefix(scope, attribute, value, is_not: false)
61-
column = column_for(scope, attribute)
62-
map = value.map { |v| "#{v}%" }
63-
clause = column.lower.matches_any(map)
64-
is_not ? scope.where.not(clause) : scope.where(clause)
65-
end
66-
67-
def filter_string_not_prefix(scope, attribute, value)
68-
filter_string_prefix(scope, attribute, value, is_not: true)
69-
end
70-
71-
def filter_string_suffix(scope, attribute, value, is_not: false)
72-
column = column_for(scope, attribute)
73-
map = value.map { |v| "%#{v}" }
74-
clause = column.lower.matches_any(map)
75-
is_not ? scope.where.not(clause) : scope.where(clause)
76-
end
77-
78-
def filter_string_not_suffix(scope, attribute, value)
79-
filter_string_suffix(scope, attribute, value, is_not: true)
80-
end
81-
8260
# Arel has different match escaping behavior before rails 5.
8361
# Since rails 4.x does not expose methods to escape LIKE statements
8462
# anyway, we just don't support proper LIKE escaping in those versions.
8563
if ::ActiveRecord.version >= Gem::Version.new("5.0.0")
8664
def filter_string_match(scope, attribute, value, is_not: false)
87-
escape_char = '\\'
88-
column = column_for(scope, attribute)
89-
map = value.map { |v|
90-
v = v.downcase
91-
v = scope.sanitize_sql_like(v)
65+
clause = sanitized_like_for(scope, attribute, value) do |v|
9266
"%#{v}%"
93-
}
94-
clause = column.lower.matches_any(map, escape_char, true)
67+
end
68+
is_not ? scope.where.not(clause) : scope.where(clause)
69+
end
70+
71+
def filter_string_prefix(scope, attribute, value, is_not: false)
72+
clause = sanitized_like_for(scope, attribute, value) do |v|
73+
"#{v}%"
74+
end
75+
is_not ? scope.where.not(clause) : scope.where(clause)
76+
end
77+
78+
def filter_string_suffix(scope, attribute, value, is_not: false)
79+
clause = sanitized_like_for(scope, attribute, value) do |v|
80+
"%#{v}"
81+
end
9582
is_not ? scope.where.not(clause) : scope.where(clause)
9683
end
9784
else
@@ -103,6 +90,28 @@ def filter_string_match(scope, attribute, value, is_not: false)
10390
clause = column.lower.matches_any(map)
10491
is_not ? scope.where.not(clause) : scope.where(clause)
10592
end
93+
94+
def filter_string_prefix(scope, attribute, value, is_not: false)
95+
column = column_for(scope, attribute)
96+
map = value.map { |v| "#{v}%" }
97+
clause = column.lower.matches_any(map)
98+
is_not ? scope.where.not(clause) : scope.where(clause)
99+
end
100+
101+
def filter_string_suffix(scope, attribute, value, is_not: false)
102+
column = column_for(scope, attribute)
103+
map = value.map { |v| "%#{v}" }
104+
clause = column.lower.matches_any(map)
105+
is_not ? scope.where.not(clause) : scope.where(clause)
106+
end
107+
end
108+
109+
def filter_string_not_prefix(scope, attribute, value)
110+
filter_string_prefix(scope, attribute, value, is_not: true)
111+
end
112+
113+
def filter_string_not_suffix(scope, attribute, value)
114+
filter_string_suffix(scope, attribute, value, is_not: true)
106115
end
107116

108117
def filter_string_not_match(scope, attribute, value)
@@ -298,6 +307,22 @@ def column_for(scope, name)
298307
table[name]
299308
end
300309
end
310+
311+
def sanitized_like_for(scope, attribute, value, &block)
312+
escape_char = '\\'
313+
column = column_for(scope, attribute)
314+
map = value.map do |v|
315+
v = v.downcase
316+
v = Sanitizer.sanitize_sql_like(v)
317+
block.call v
318+
end
319+
320+
column.lower.matches_any(map, escape_char, true)
321+
end
322+
323+
class Sanitizer
324+
extend ::ActiveRecord::Sanitization::ClassMethods
325+
end
301326
end
302327
end
303328
end

spec/integration/rails/finders_spec.rb

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,24 @@ def resource
231231
it "executes case-insensitive prefix query" do
232232
expect(ids).to eq([author2.id, author3.id])
233233
end
234+
235+
if ::ActiveRecord.version >= Gem::Version.new("5.0")
236+
context 'when match string includes % characters' do
237+
let(:value) { {prefix: "%ild"} }
238+
239+
let!(:author_with_percent) do
240+
Legacy::Author.create!(first_name: "%ildcard")
241+
end
242+
243+
let!(:normal_author) do
244+
Legacy::Author.create!(first_name: "wildcard")
245+
end
246+
247+
it "does not use the provided % as a wildcard character" do
248+
expect(ids).to eq([author_with_percent.id])
249+
end
250+
end
251+
end
234252
end
235253

236254
context "!prefix" do
@@ -247,6 +265,24 @@ def resource
247265
it "executes case-insensitive suffix query" do
248266
expect(ids).to eq([author2.id, author3.id])
249267
end
268+
269+
if ::ActiveRecord.version >= Gem::Version.new("5.0")
270+
context 'when match string includes % characters' do
271+
let(:value) { {suffix: "car%"} }
272+
273+
let!(:author_with_percent) do
274+
Legacy::Author.create!(first_name: "Wildcar%")
275+
end
276+
277+
let!(:normal_author) do
278+
Legacy::Author.create!(first_name: "Wildcard")
279+
end
280+
281+
it "does not use the provided % as a wildcard character" do
282+
expect(ids).to eq([author_with_percent.id])
283+
end
284+
end
285+
end
250286
end
251287

252288
context "!suffix" do

0 commit comments

Comments
 (0)