cancancan 3.6.1 → 3.7.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.
@@ -1,13 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'controller_resource_loader.rb'
4
3
  module CanCan
5
4
  # Handle the load and authorization controller logic
6
5
  # so we don't clutter up all controllers with non-interface methods.
7
6
  # This class is used internally, so you do not need to call methods directly on it.
8
7
  class ControllerResource # :nodoc:
9
- include ControllerResourceLoader
10
-
11
8
  def self.add_before_action(controller_class, method, *args)
12
9
  options = args.extract_options!
13
10
  resource_name = args.first
@@ -26,7 +23,7 @@ module CanCan
26
23
  @controller = controller
27
24
  @params = controller.params
28
25
  @options = args.extract_options!
29
- @name = args.first
26
+ @name = args.first&.to_sym
30
27
  end
31
28
 
32
29
  def load_and_authorize_resource
@@ -34,6 +31,16 @@ module CanCan
34
31
  authorize_resource
35
32
  end
36
33
 
34
+ def load_resource
35
+ return if skip?(:load)
36
+
37
+ if load_instance?
38
+ self.resource_instance ||= load_resource_instance
39
+ elsif load_collection?
40
+ self.collection_instance ||= load_collection
41
+ end
42
+ end
43
+
37
44
  def authorize_resource
38
45
  return if skip?(:authorize)
39
46
 
@@ -54,10 +61,34 @@ module CanCan
54
61
 
55
62
  protected
56
63
 
64
+ def name
65
+ @name || name_from_controller
66
+ end
67
+
68
+ def name_from_controller
69
+ @params[:controller].split('/').last.singularize
70
+ end
71
+
72
+ def namespace
73
+ @params[:controller].split('/')[0..-2]
74
+ end
75
+
76
+ def namespaced_name
77
+ [namespace, name].join('/').singularize.camelize.safe_constantize || name
78
+ end
79
+
80
+ def instance_name
81
+ @options[:instance_name] || name
82
+ end
83
+
57
84
  # Returns the class used for this resource. This can be overridden by the :class option.
58
85
  # If +false+ is passed in it will use the resource name as a symbol in which case it should
59
86
  # only be used for authorization, not loading since there's no class to load through.
60
87
  def resource_class
88
+ @resource_class ||= resource_class_from_options
89
+ end
90
+
91
+ def resource_class_from_options
61
92
  case @options[:class]
62
93
  when false
63
94
  name.to_sym
@@ -70,12 +101,20 @@ module CanCan
70
101
  end
71
102
  end
72
103
 
73
- def load_instance?
74
- parent? || member_action?
104
+ def resource_class_with_parent
105
+ parent_resource ? { parent_resource => resource_class } : resource_class
75
106
  end
76
107
 
77
- def load_collection?
78
- resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
108
+ def new_actions
109
+ %i[new create] + Array(@options[:new]).map(&:to_sym)
110
+ end
111
+
112
+ def collection_actions
113
+ [:index] + Array(@options[:collection]).map(&:to_sym)
114
+ end
115
+
116
+ def save_actions
117
+ %i[create update]
79
118
  end
80
119
 
81
120
  def member_action?
@@ -84,8 +123,20 @@ module CanCan
84
123
  !collection_actions.include?(@params[:action].to_sym))
85
124
  end
86
125
 
87
- def resource_class_with_parent
88
- parent_resource ? { parent_resource => resource_class } : resource_class
126
+ def load_instance?
127
+ parent? || member_action?
128
+ end
129
+
130
+ def load_collection?
131
+ resource_base.respond_to?(:accessible_by) && !current_ability.has_block?(authorization_action, resource_class)
132
+ end
133
+
134
+ def parent_authorization_action
135
+ @options[:parent_action] || :show
136
+ end
137
+
138
+ def authorization_action
139
+ parent? ? parent_authorization_action : @params[:action].to_sym
89
140
  end
90
141
 
91
142
  def resource_instance=(instance)
@@ -108,26 +159,205 @@ module CanCan
108
159
  @controller.instance_variable_get("@#{instance_name.to_s.pluralize}")
109
160
  end
110
161
 
162
+ def load_collection
163
+ resource_base.accessible_by(current_ability, authorization_action)
164
+ end
165
+
166
+ def load_resource_instance
167
+ if !parent? && new_actions.include?(@params[:action].to_sym)
168
+ build_resource
169
+ elsif id_param || @options[:singleton]
170
+ find_resource
171
+ end
172
+ end
173
+
174
+ def fetch_parent(name)
175
+ if @controller.instance_variable_defined? "@#{name}"
176
+ @controller.instance_variable_get("@#{name}")
177
+ elsif @controller.respond_to?(name, true)
178
+ @controller.send(name)
179
+ end
180
+ end
181
+
182
+ # The object to load this resource through.
183
+ def parent_resource
184
+ parent_name && fetch_parent(parent_name)
185
+ end
186
+
187
+ def parent_name
188
+ @options[:through] && [@options[:through]].flatten.detect { |i| fetch_parent(i) }
189
+ end
190
+
191
+ def resource_base_through_parent_resource
192
+ if @options[:singleton]
193
+ resource_class
194
+ else
195
+ parent_resource.send(@options[:through_association] || name.to_s.pluralize)
196
+ end
197
+ end
198
+
199
+ def resource_base_through
200
+ if parent_resource
201
+ resource_base_through_parent_resource
202
+ elsif @options[:shallow]
203
+ resource_class
204
+ else
205
+ # maybe this should be a record not found error instead?
206
+ raise AccessDenied.new(nil, authorization_action, resource_class)
207
+ end
208
+ end
209
+
210
+ # The object that methods (such as "find", "new" or "build") are called on.
211
+ # If the :through option is passed it will go through an association on that instance.
212
+ # If the :shallow option is passed it will use the resource_class if there's no parent
213
+ # If the :singleton option is passed it won't use the association because it needs to be handled later.
214
+ def resource_base
215
+ @options[:through] ? resource_base_through : resource_class
216
+ end
217
+
218
+ def find_resource
219
+ singleton_association_name = @options[:through_association] || name
220
+ if @options[:singleton] && parent_resource.respond_to?(singleton_association_name)
221
+ parent_resource.send(singleton_association_name)
222
+ elsif @options[:find_by]
223
+ find_resource_using_find_by
224
+ else
225
+ adapter.find(resource_base, id_param)
226
+ end
227
+ end
228
+
229
+ def find_resource_using_find_by
230
+ find_by_dynamic_finder || find_by_find_by_finder || resource_base.send(@options[:find_by], id_param)
231
+ end
232
+
233
+ def find_by_dynamic_finder
234
+ method_name = "find_by_#{@options[:find_by]}!"
235
+ resource_base.send(method_name, id_param) if resource_base.respond_to? method_name
236
+ end
237
+
238
+ def find_by_find_by_finder
239
+ resource_base.find_by(@options[:find_by].to_sym => id_param) if resource_base.respond_to? :find_by
240
+ end
241
+
242
+ def id_param
243
+ # A whitespace-only id is blank but given, so it must also be looked up.
244
+ id = @params[id_param_key].to_s
245
+ id unless id.empty?
246
+ end
247
+
248
+ def id_param_key
249
+ if @options[:id_param]
250
+ @options[:id_param]
251
+ else
252
+ parent? ? :"#{name}_id" : :id
253
+ end
254
+ end
255
+
256
+ def build_resource
257
+ resource = resource_base.new(resource_params || {})
258
+ assign_attributes(resource)
259
+ end
260
+
261
+ def assign_attributes(resource)
262
+ assign_parent(resource) if @options[:singleton] && parent_resource
263
+ initial_attributes.each do |attr_name, value|
264
+ resource.send("#{attr_name}=", value)
265
+ end
266
+ resource
267
+ end
268
+
269
+ # The parent is assigned through the setter named after the :through option when the resource has one,
270
+ # otherwise through the inverse of the association on the parent.
271
+ def assign_parent(resource)
272
+ setter = "#{parent_name}="
273
+ setter = "#{inverse_association_name}=" unless resource.respond_to?(setter)
274
+ resource.send(setter, parent_resource)
275
+ end
276
+
277
+ def inverse_association_name
278
+ association_name = @options[:through_association] || name
279
+ inverse = inverse_of_association(parent_resource.class, association_name)
280
+ return inverse if inverse
281
+
282
+ raise Error, "Cannot assign the parent to the singleton resource: #{resource_class} has no " \
283
+ "#{parent_name}= method and the association #{association_name} on #{parent_resource.class} " \
284
+ 'has no inverse. Define inverse_of on the association or use a through option named after it.'
285
+ end
286
+
287
+ def inverse_of_association(parent_class, association_name)
288
+ return unless parent_class.respond_to?(:reflect_on_association)
289
+
290
+ reflection = parent_class.reflect_on_association(association_name)
291
+ reflection && (reflection.inverse_of&.name || reflection.options[:as])
292
+ end
293
+
294
+ def initial_attributes
295
+ current_ability.attributes_for(@params[:action].to_sym, resource_class).delete_if do |key, _value|
296
+ resource_params&.include?(key)
297
+ end
298
+ end
299
+
300
+ # The params method of the controller can have side effects, so it is called at most once per request.
301
+ def resource_params
302
+ return @resource_params if defined?(@resource_params)
303
+
304
+ @resource_params =
305
+ if parameters_require_sanitizing? && params_method.present?
306
+ sanitize_parameters
307
+ else
308
+ resource_params_by_namespaced_name
309
+ end
310
+ end
311
+
111
312
  def parameters_require_sanitizing?
112
313
  save_actions.include?(@params[:action].to_sym) || resource_params_by_namespaced_name.present?
113
314
  end
114
315
 
115
- def instance_name
116
- @options[:instance_name] || name
316
+ def resource_params_by_key(key)
317
+ return unless @options[key] && @params.key?(extract_key(@options[key]))
318
+
319
+ @params[extract_key(@options[key])]
117
320
  end
118
321
 
119
- def collection_actions
120
- [:index] + Array(@options[:collection])
322
+ def resource_params_by_namespaced_name
323
+ resource_params_by_key(:instance_name) || resource_params_by_key(:class) || (
324
+ params = @params[extract_key(namespaced_name)]
325
+ params.respond_to?(:to_h) ? params : nil)
121
326
  end
122
327
 
123
- def save_actions
124
- %i[create update]
328
+ def sanitize_parameters
329
+ case params_method
330
+ when Symbol
331
+ @controller.send(params_method)
332
+ when String
333
+ @controller.instance_eval(params_method)
334
+ when Proc
335
+ params_method.call(@controller)
336
+ end
337
+ end
338
+
339
+ def params_methods
340
+ methods = ["#{@params[:action]}_params".to_sym, "#{name}_params".to_sym, :resource_params]
341
+ methods.unshift(@options[:param_method]) if @options[:param_method].present?
342
+ methods
343
+ end
344
+
345
+ def params_method
346
+ params_methods.each do |method|
347
+ return method if (method.is_a?(Symbol) && @controller.respond_to?(method, true)) ||
348
+ method.is_a?(String) || method.is_a?(Proc)
349
+ end
350
+ nil
125
351
  end
126
352
 
127
353
  private
128
354
 
129
355
  def action_exists_in?(options)
130
- Array(options).include?(@params[:action].to_sym)
356
+ Array(options).map(&:to_sym).include?(@params[:action].to_sym)
357
+ end
358
+
359
+ def extract_key(value)
360
+ value.to_s.underscore.tr('/', '_')
131
361
  end
132
362
 
133
363
  def adapter
@@ -40,6 +40,15 @@ module CanCan
40
40
  parent.class.name.downcase.to_sym
41
41
  end
42
42
 
43
+ # Override if conditions on the parent could be under other keys, like a foreign key
44
+ def self.parent_condition_keys(parent, child)
45
+ [parent_condition_name(parent, child)]
46
+ end
47
+
48
+ def self.conditions_only_on_parent?(parent, child, conditions)
49
+ conditions.is_a?(Hash) && (conditions.keys.map(&:to_sym) - parent_condition_keys(parent, child)).empty?
50
+ end
51
+
43
52
  # Used above override_conditions_hash_matching to determine if this model adapter will override the
44
53
  # matching behavior for nested subject.
45
54
  # If this returns true then nested_subject_matches_conditions? will be called.
@@ -14,8 +14,8 @@ module CanCan
14
14
  def self.matches_condition?(subject, name, value)
15
15
  return super if Array.wrap(value).all? { |x| x.is_a? Integer }
16
16
 
17
- attribute = subject.send(name)
18
- raw_attribute = subject.class.send(name.to_s.pluralize)[attribute]
17
+ attribute = subject.send(name).to_s
18
+ raw_attribute = subject.class.send(name.to_s.pluralize)[attribute].to_s
19
19
  !(Array(value).map(&:to_s) & [attribute, raw_attribute]).empty?
20
20
  end
21
21
 
@@ -26,34 +26,62 @@ module CanCan
26
26
  end
27
27
 
28
28
  def strategy_class
29
- strategy_class_name = CanCan.accessible_by_strategy.to_s.camelize
30
- CanCan::ModelAdapters::Strategies.const_get(strategy_class_name)
29
+ strategy = CanCan.accessible_by_strategy
30
+ return strategy if strategy.is_a?(Class)
31
+
32
+ CanCan::ModelAdapters::Strategies.const_get(strategy.to_s.camelize)
31
33
  end
32
34
 
33
- def sanitize_sql(conditions)
35
+ def sanitize_sql(conditions, conditions_extractor = nil)
34
36
  if conditions.is_a?(Hash)
35
- sanitize_sql_activerecord5(conditions)
37
+ sanitize_sql_activerecord5(conditions, conditions_extractor)
36
38
  else
37
39
  @model_class.send(:sanitize_sql, conditions)
38
40
  end
39
41
  end
40
42
 
41
- def sanitize_sql_activerecord5(conditions)
42
- table = @model_class.send(:arel_table)
43
- table_metadata = ActiveRecord::TableMetadata.new(@model_class, table)
44
- predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
43
+ def where_node(conditions, conditions_extractor)
44
+ Arel::Nodes::And.new(predicates(conditions, conditions_extractor))
45
+ end
46
+
47
+ def sanitize_sql_activerecord5(conditions, conditions_extractor)
48
+ predicates(conditions, conditions_extractor).map { |b| visit_nodes(b) }.join(' AND ')
49
+ end
45
50
 
46
- predicate_builder.build_from_hash(conditions.stringify_keys).map { |b| visit_nodes(b) }.join(' AND ')
51
+ def predicates(conditions, conditions_extractor)
52
+ conditions.flat_map do |key, value|
53
+ table_class = value.is_a?(Hash) && conditions_extractor&.model_class_for(key)
54
+ if table_class
55
+ predicate_builder(table_class, table_class.arel_table.alias(key.to_s)).build_from_hash(value.stringify_keys)
56
+ else
57
+ predicate_builder(@model_class, @model_class.send(:arel_table)).build_from_hash(key.to_s => value)
58
+ end
59
+ end
60
+ end
61
+
62
+ def predicate_builder(model_class, table)
63
+ ActiveRecord::PredicateBuilder.new(ActiveRecord::TableMetadata.new(model_class, table))
47
64
  end
48
65
 
49
66
  def visit_nodes(node)
50
67
  # Rails 5.2 adds a BindParam node that prevents the visitor method from properly compiling the SQL query
51
68
  if self.class.version_greater_or_equal?('5.2.0')
52
- connection = @model_class.send(:connection)
69
+ connection = model_connection
53
70
  collector = Arel::Collectors::SubstituteBinds.new(connection, Arel::Collectors::SQLString.new)
54
71
  connection.visitor.accept(node, collector).value
55
72
  else
56
- @model_class.send(:connection).visitor.compile(node)
73
+ model_connection.visitor.compile(node)
74
+ end
75
+ end
76
+
77
+ # Rails 7.2 soft-deprecated ActiveRecord::Base.connection in favour of
78
+ # lease_connection, which better describes its lifecycle. Fall back to
79
+ # connection for Rails < 7.2 where lease_connection is not defined.
80
+ def model_connection
81
+ if @model_class.respond_to?(:lease_connection)
82
+ @model_class.lease_connection
83
+ else
84
+ @model_class.send(:connection)
57
85
  end
58
86
  end
59
87
  end
@@ -23,6 +23,8 @@ module CanCan
23
23
  else
24
24
  @rules
25
25
  end
26
+ # The normalizers change the rules. Work on copies, so the rules of the ability stay the same.
27
+ @compressed_rules = @compressed_rules.map(&:dup)
26
28
  StiNormalizer.normalize(@compressed_rules)
27
29
  ConditionsNormalizer.normalize(model_class, @compressed_rules)
28
30
  end
@@ -43,7 +45,16 @@ module CanCan
43
45
  false
44
46
  end
45
47
 
48
+ def parent_condition_keys(parent, child)
49
+ [parent_condition_name(parent, child), parent_child_foreign_key(parent, child)].compact
50
+ end
51
+
46
52
  def parent_child_conditions(parent, child, all_conditions)
53
+ foreign_key = parent_child_foreign_key(parent, child)
54
+ foreign_key.nil? ? nil : all_conditions[foreign_key]
55
+ end
56
+
57
+ def parent_child_foreign_key(parent, child)
47
58
  child_class = child.is_a?(Class) ? child : child.class
48
59
  parent_class = parent.is_a?(Class) ? parent : parent.class
49
60
 
@@ -54,11 +65,9 @@ module CanCan
54
65
 
55
66
  # Search again in case of polymorphic associations, this time matching on the :has_many side
56
67
  # via the :as option, as well as klass
57
- foreign_key ||= parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
68
+ foreign_key || parent_class.reflect_on_all_associations(:has_many).find do |has_many_assoc|
58
69
  matching_parent_child_polymorphic_association(has_many_assoc, child_class)
59
70
  end&.foreign_key&.to_sym
60
-
61
- foreign_key.nil? ? nil : all_conditions[foreign_key]
62
71
  end
63
72
 
64
73
  def matching_parent_child_polymorphic_association(parent_assoc, child_class)
@@ -110,26 +119,22 @@ module CanCan
110
119
  # query(:manage, User).conditions # => "not (self_managed = 't') AND ((manager_id = 1) OR (id = 1))"
111
120
  #
112
121
  def conditions
113
- conditions_extractor = ConditionsExtractor.new(@model_class)
114
- if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
115
- # Return the conditions directly if there's just one definition
116
- conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
117
- else
118
- extract_multiple_conditions(conditions_extractor, @compressed_rules)
119
- end
122
+ extract_conditions(ConditionsExtractor.new(@model_class, joins))
120
123
  end
121
124
 
122
125
  def extract_multiple_conditions(conditions_extractor, rules)
123
- rules.reverse.inject(false_sql) do |sql, rule|
124
- merge_conditions(sql, conditions_extractor.tableize_conditions(rule.conditions).dup, rule.base_behavior)
126
+ sql, = rules.reverse.inject([false_sql, false]) do |(sql, or_chain), rule|
127
+ conditions_hash = conditions_extractor.tableize_conditions(rule.conditions).dup
128
+ merge_conditions(sql, or_chain, conditions_hash, rule.base_behavior, conditions_extractor)
125
129
  end
130
+ sql
126
131
  end
127
132
 
128
133
  def database_records
129
134
  if override_scope
130
- @model_class.where(nil).merge(override_scope)
135
+ merge_override_scope
131
136
  elsif @model_class.respond_to?(:where) && @model_class.respond_to?(:joins)
132
- build_relation(conditions)
137
+ build_relation(where_conditions)
133
138
  else
134
139
  @model_class.all(conditions: conditions, joins: joins)
135
140
  end
@@ -155,6 +160,29 @@ module CanCan
155
160
 
156
161
  private
157
162
 
163
+ def extract_conditions(conditions_extractor)
164
+ if @compressed_rules.size == 1 && @compressed_rules.first.base_behavior
165
+ # Return the conditions directly if there's just one definition
166
+ conditions_extractor.tableize_conditions(@compressed_rules.first.conditions).dup
167
+ else
168
+ extract_multiple_conditions(conditions_extractor, @compressed_rules)
169
+ end
170
+ end
171
+
172
+ # Active Record cannot find the model of an aliased table in a hash of conditions,
173
+ # so the conditions on associations are converted to SQL with the model of each table.
174
+ def where_conditions
175
+ conditions_extractor = ConditionsExtractor.new(@model_class, joins)
176
+ conditions = extract_conditions(conditions_extractor)
177
+ return conditions unless conditions.is_a?(Hash) && conditions.values.any? { |value| value.is_a?(Hash) }
178
+
179
+ where_node(conditions, conditions_extractor)
180
+ end
181
+
182
+ def where_node(conditions, conditions_extractor)
183
+ sanitize_sql(conditions, conditions_extractor)
184
+ end
185
+
158
186
  # Removes empty hashes and moves everything into arrays.
159
187
  def deep_clean(joins_hash)
160
188
  joins_hash.map { |name, nested| nested.empty? ? name : { name => deep_clean(nested) } }
@@ -171,6 +199,17 @@ module CanCan
171
199
  end
172
200
  end
173
201
 
202
+ # `merge` replaces the conditions of the base relation on the columns the scope also constrains,
203
+ # so the conditions it dropped, like the one of an association like `category.articles`, are applied again.
204
+ # The conditions the scope removes with `unscope`, like the ones of a default scope, stay removed.
205
+ def merge_override_scope
206
+ merged = @model_class.where(nil).merge(override_scope)
207
+ base = @model_class.where(nil)
208
+ base = base.unscope(*override_scope.unscope_values) if override_scope.unscope_values.any?
209
+ dropped = base.where_clause.send(:predicates) - merged.where_clause.send(:predicates)
210
+ dropped.inject(merged) { |relation, predicate| relation.where(predicate) }
211
+ end
212
+
174
213
  def override_scope
175
214
  conditions = @compressed_rules.map(&:conditions).compact
176
215
  return unless conditions.any? { |c| c.is_a?(ActiveRecord::Relation) }
@@ -186,26 +225,45 @@ module CanCan
186
225
  "Instead use a hash or SQL for #{rule_found.actions.first} #{rule_found.subjects.first} ability."
187
226
  end
188
227
 
189
- def merge_conditions(sql, conditions_hash, behavior)
228
+ # Returns the merged sql and whether it is a flat chain of OR conditions.
229
+ def merge_conditions(sql, or_chain, conditions_hash, behavior, conditions_extractor)
190
230
  if conditions_hash.blank?
191
- behavior ? true_sql : false_sql
231
+ [behavior ? true_sql : false_sql, false]
192
232
  else
193
- merge_non_empty_conditions(behavior, conditions_hash, sql)
233
+ merge_non_empty_conditions(behavior, sanitize_sql(conditions_hash, conditions_extractor), sql, or_chain)
194
234
  end
195
235
  end
196
236
 
197
- def merge_non_empty_conditions(behavior, conditions_hash, sql)
198
- conditions = sanitize_sql(conditions_hash)
237
+ # Consecutive `can` rules are joined in a flat chain of ORs, so that the nesting of the query only grows
238
+ # when `can` and `cannot` rules alternate. Deeply nested conditions overflow the parser of SQLite.
239
+ def merge_non_empty_conditions(behavior, conditions, sql, or_chain)
199
240
  case sql
200
241
  when true_sql
201
- behavior ? true_sql : "not (#{conditions})"
242
+ behavior ? [true_sql, false] : [not_conditions(conditions), false]
202
243
  when false_sql
203
- behavior ? conditions : false_sql
244
+ behavior ? [conditions, false] : [false_sql, false]
245
+ else
246
+ merge_with_previous_conditions(behavior, conditions, sql, or_chain)
247
+ end
248
+ end
249
+
250
+ def merge_with_previous_conditions(behavior, conditions, sql, or_chain)
251
+ if behavior
252
+ [or_conditions(conditions, sql, or_chain), true]
204
253
  else
205
- behavior ? "(#{conditions}) OR (#{sql})" : "not (#{conditions}) AND (#{sql})"
254
+ ["#{not_conditions(conditions)} AND (#{sql})", false]
206
255
  end
207
256
  end
208
257
 
258
+ # A cannot rule only excludes the records that match its conditions: NULL does not match.
259
+ def not_conditions(conditions)
260
+ "(#{conditions}) IS NOT TRUE"
261
+ end
262
+
263
+ def or_conditions(conditions, sql, or_chain)
264
+ "(#{conditions}) OR #{or_chain ? sql : "(#{sql})"}"
265
+ end
266
+
209
267
  def false_sql
210
268
  sanitize_sql(['?=?', true, false])
211
269
  end
@@ -214,7 +272,7 @@ module CanCan
214
272
  sanitize_sql(['?=?', true, true])
215
273
  end
216
274
 
217
- def sanitize_sql(conditions)
275
+ def sanitize_sql(conditions, _conditions_extractor = nil)
218
276
  @model_class.send(:sanitize_sql, conditions)
219
277
  end
220
278
  end