NodesCharsBranches
346 / 3502564 / 258319 / 22
DeepCover v0.1.16
1# frozen_string_literal: true
2
3module ActiveRecord
4 module Sanitization
5 extend ActiveSupport::Concern
6
7 module ClassMethods
8 private
9
10 # Accepts an array or string of SQL conditions and sanitizes
11 # them into a valid SQL fragment for a WHERE clause.
12 #
13 # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
14 # # => "name='foo''bar' and group_id=4"
15 #
16 # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
17 # # => "name='foo''bar' and group_id='4'"
18 #
19 # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
20 # # => "name='foo''bar' and group_id='4'"
21 #
22 # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
23 # # => "name='foo''bar' and group_id='4'"
24 def sanitize_sql_for_conditions(condition) # :doc:
25 return nil if condition.blank?
26
27 case condition
28 when Array; sanitize_sql_array(condition)
29 else condition
30 end
31 end
32 alias :sanitize_sql :sanitize_sql_for_conditions
33
34 # Accepts an array, hash, or string of SQL conditions and sanitizes
35 # them into a valid SQL fragment for a SET clause.
36 #
37 # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
38 # # => "name=NULL and group_id=4"
39 #
40 # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
41 # # => "name=NULL and group_id=4"
42 #
43 # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
44 # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
45 #
46 # sanitize_sql_for_assignment("name=NULL and group_id='4'")
47 # # => "name=NULL and group_id='4'"
48 def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc:
49 case assignments
50 when Array; sanitize_sql_array(assignments)
51 when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
52 else assignments
53 end
54 end
55
56 # Accepts an array, or string of SQL conditions and sanitizes
57 # them into a valid SQL fragment for an ORDER clause.
58 #
59 # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
60 # # => "field(id, 1,3,2)"
61 #
62 # sanitize_sql_for_order("id ASC")
63 # # => "id ASC"
64 def sanitize_sql_for_order(condition) # :doc:
65 if condition.is_a?(Array) && condition.first.to_s.include?("?")
66 enforce_raw_sql_whitelist([condition.first],
67 whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
68 )
69
70 # Ensure we aren't dealing with a subclass of String that might
71 # override methods we use (eg. Arel::Nodes::SqlLiteral).
72 if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
73 condition = [String.new(condition.first), *condition[1..-1]]
74 end
75
76 Arel.sql(sanitize_sql_array(condition))
77 else
78 condition
79 end
80 end
81
82 # Accepts a hash of SQL conditions and replaces those attributes
83 # that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
84 # relationship with their expanded aggregate attribute values.
85 #
86 # Given:
87 #
88 # class Person < ActiveRecord::Base
89 # composed_of :address, class_name: "Address",
90 # mapping: [%w(address_street street), %w(address_city city)]
91 # end
92 #
93 # Then:
94 #
95 # { address: Address.new("813 abc st.", "chicago") }
96 # # => { address_street: "813 abc st.", address_city: "chicago" }
97 def expand_hash_conditions_for_aggregates(attrs) # :doc:
98 expanded_attrs = {}
99 attrs.each do |attr, value|
100 if aggregation = reflect_on_aggregation(attr.to_sym)
101 mapping = aggregation.mapping
102 mapping.each do |field_attr, aggregate_attr|
103 if mapping.size == 1 && !value.respond_to?(aggregate_attr)
104 expanded_attrs[field_attr] = value
105 else
106 expanded_attrs[field_attr] = value.send(aggregate_attr)
107 end
108 end
109 else
110 expanded_attrs[attr] = value
111 end
112 end
113 expanded_attrs
114 end
115
116 # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
117 #
118 # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
119 # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
120 def sanitize_sql_hash_for_assignment(attrs, table) # :doc:
121 c = connection
122 attrs.map do |attr, value|
123 type = type_for_attribute(attr.to_s)
124 value = type.serialize(type.cast(value))
125 "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
126 end.join(", ")
127 end
128
129 # Sanitizes a +string+ so that it is safe to use within an SQL
130 # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
131 #
132 # sanitize_sql_like("100%")
133 # # => "100\\%"
134 #
135 # sanitize_sql_like("snake_cased_string")
136 # # => "snake\\_cased\\_string"
137 #
138 # sanitize_sql_like("100%", "!")
139 # # => "100!%"
140 #
141 # sanitize_sql_like("snake_cased_string", "!")
142 # # => "snake!_cased!_string"
143 def sanitize_sql_like(string, escape_character = "\\") # :doc:
144 pattern = Regexp.union(escape_character, "%", "_")
145 string.gsub(pattern) { |x| [escape_character, x].join }
146 end
147
148 # Accepts an array of conditions. The array has each value
149 # sanitized and interpolated into the SQL statement.
150 #
151 # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
152 # # => "name='foo''bar' and group_id=4"
153 #
154 # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
155 # # => "name='foo''bar' and group_id=4"
156 #
157 # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
158 # # => "name='foo''bar' and group_id='4'"
159 def sanitize_sql_array(ary) # :doc:
160 statement, *values = ary
161 if values.first.is_a?(Hash) && /:\w+/.match?(statement)
162 replace_named_bind_variables(statement, values.first)
163 elsif statement.include?("?")
164 replace_bind_variables(statement, values)
165 elsif statement.blank?
166 statement
167 else
168 statement % values.collect { |value| connection.quote_string(value.to_s) }
169 end
170 end
171
172 def replace_bind_variables(statement, values)
173 raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
174 bound = values.dup
175 c = connection
176 statement.gsub(/\?/) do
177 replace_bind_variable(bound.shift, c)
178 end
179 end
180
181 def replace_bind_variable(value, c = connection)
182 if ActiveRecord::Relation === value
183 value.to_sql
184 else
185 quote_bound_value(value, c)
186 end
187 end
188
189 def replace_named_bind_variables(statement, bind_vars)
190 statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
191 if $1 == ":" # skip postgresql casts
192 match # return the whole match
193 elsif bind_vars.include?(match = $2.to_sym)
194 replace_bind_variable(bind_vars[match])
195 else
196 raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
197 end
198 end
199 end
200
201 def quote_bound_value(value, c = connection)
202 if value.respond_to?(:map) && !value.acts_like?(:string)
203 if value.respond_to?(:empty?) && value.empty?
204 c.quote(nil)
205 else
206 value.map { |v| c.quote(v) }.join(",")
207 end
208 else
209 c.quote(value)
210 end
211 end
212
213 def raise_if_bind_arity_mismatch(statement, expected, provided)
214 unless expected == provided
215 raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
216 end
217 end
218 end
219 end
220end