class Ransack::Context
def allowlist(klass, method, *fallbacks)
block's list of everything that exists is used, and otherwise the
decision to the controller: when the parameters were permitted, the
the controller permitted. A model that defines none has left the
own `method` is always asked, so its list applies on top of whatever
The allowlist a search consults on `klass`. A model that defines its
def allowlist(klass, method, *fallbacks) if permitted && [method, *fallbacks].none? { |m| klass.ransackable_list_defined?(m) } yield else klass.public_send(method, auth_object) end end
def association_path(str, base = @base)
def association_path(str, base = @base) return ''.freeze unless key_within_depth_limit?(str) base = klassify(base) str ||= ''.freeze path = [] segments = str.split(Constants::UNDERSCORE) association_parts = [] unless segments.empty? while !segments.empty? && !base.columns_hash[segments.join(Constants::UNDERSCORE)] && association_parts << segments.shift assoc, klass = unpolymorphize_association( association_parts.join(Constants::UNDERSCORE) ) next unless found_assoc = get_association(assoc, base) # A polymorphic association is only complete once its # `_of_Model_type` suffix has been consumed; matching on the bare # name would ask the reflection for a class it cannot know (#1557). next if found_assoc.polymorphic? && klass.nil? path += association_parts association_parts = [] base = klassify(klass || found_assoc) end end path.join(Constants::UNDERSCORE) end
def associations_for(klass)
def associations_for(klass) allowlist(klass, :ransackable_associations) { klass.authorizable_ransackable_associations } end
def attributes_for(klass)
def attributes_for(klass) allowlist(klass, :ransackable_attributes) { klass.authorizable_ransackable_attributes } end
def bind(object, str)
def bind(object, str) return nil unless str object.parent, object.attr_name = bind_pair_for(str) end
def bind_pair_for(key)
def bind_pair_for(key) @bind_pairs ||= {} @bind_pairs[key] ||= begin parent, attr_name = get_parent_and_attribute_name(key.to_s) [parent, attr_name] if parent && attr_name end end
def cast_scope_args(args)
def cast_scope_args(args) if args.is_a?(Array) args = args.map(&method(:cast_scope_args)) end if Constants::TRUE_VALUES.include? args true elsif Constants::FALSE_VALUES.include? args false else args end end
def chain_scope(scope, args)
given, so `false` reaches it like any other value and it can be driven
`ransackable_scopes_skip_sanitize_args` has asked to see its values as
scope is not applied at all. A scope listed in
A bare `false` normally means "this checkbox was not ticked" and the
def chain_scope(scope, args) return unless @klass.method(scope) return if args == false && !ransackable_scope_skip_sanitize_args?(scope, @klass) @object = if scope_arity(scope) < 1 && args == true @object.public_send(scope) elsif scope_arity(scope) == 1 && args.is_a?(Array) # For scopes with arity 1, pass the array as a single argument instead of splatting @object.public_send(scope, args) else @object.public_send(scope, *args) end end
def contextualize(str)
Convert a string representing a chain of associations and an attribute
def contextualize(str) parent, attr_name = bind_pair_for(str) table_for(parent)[attr_name] end
def dialect
where generated SQL differs by database. See
The SQL dialect the search will be rendered in, for the few places
def dialect raise NotImplementedError, "#{self.class} must implement #dialect" end
def for(object, options = {})
def for(object, options = {}) Ransack::Context.resolvers.each do |resolver| context = resolver.call(object, options) return context if context end raise ArgumentError, "Don't know what context to use for #{object}" end
def initialize(object, options = {})
def initialize(object, options = {}) @object = relation_for(object) @klass = @object.klass @join_type = options[:join_type] || Arel::Nodes::OuterJoin @search_key = options[:search_key] || Ransack.options[:search_key] @associations_pot = {} @tables_pot = {} @lock_associations = [] @join_dependency = join_dependency(@object) @base = @join_dependency.instance_variable_get(:@join_root) end
def key_within_depth_limit?(key)
an over-long key is treated as unknown rather than parsed
superlinear in the segment count and the key is attacker-controlled, so
parse. The attribute / association / predicate parsing that follows is
False when a key has more `_`-separated segments than Ransack will
def key_within_depth_limit?(key) key.to_s.count(Constants::UNDERSCORE) <= Constants::MAX_KEY_DEPTH end
def klassify(obj)
Each ORM integration decides what counts as a model, so this is defined
The model class behind a search object, an association node or a class.
def klassify(obj) raise NotImplementedError, "#{self.class} must implement #klassify" end
def null_sentinel
Condition captures this when it is built, so a later search reusing
set, including to `false` to turn the feature off for one search. A
Falls back to the global option unless this search's own option was
def null_sentinel @null_sentinel.nil? ? Ransack.options[:null_sentinel] : @null_sentinel end
def polymorphic_class(name)
`Kernel.const_get` used to raise `NameError` straight from the URL
raised as `InvalidSearchError` under `ignore_unknown_conditions`.
and the whole key fails as any other unknown attribute does, ignored or
model to search, so the suffix is then no polymorphic reference at all
something that is no module at all (`ENV::Person`). None of those is a
lowercase word, a module, a class with no table, a path through
string, so it can name anything: a constant that does not exist, a
The class named in an `_of_Model_type` suffix comes from the query
def polymorphic_class(name) klass = name.safe_constantize klass if klass.is_a?(Class) && searchable_class?(klass) rescue TypeError # `safe_constantize` swallows a missing constant but not a namespace # that is not a module, which Ruby reports as a TypeError. nil end
def ransackable_alias(str)
def ransackable_alias(str) klass._ransack_aliases.fetch(str, klass._ransack_aliases.fetch(str.to_sym, str)) end
def ransackable_association?(str, klass)
def ransackable_association?(str, klass) associations_for(klass).any? { |s| s.to_sym == str.to_sym } end
def ransackable_attribute?(str, klass)
def ransackable_attribute?(str, klass) attributes_for(klass).any? { |s| s.to_sym == str.to_sym } end
def ransackable_scope?(str, klass)
def ransackable_scope?(str, klass) klass.ransackable_scopes(auth_object).any? { |s| s.to_sym == str.to_sym } end
def ransackable_scope_skip_sanitize_args?(str, klass)
def ransackable_scope_skip_sanitize_args?(str, klass) klass.ransackable_scopes_skip_sanitize_args.any? { |s| s.to_sym == str.to_sym } end
def ransortable_attribute?(str, klass)
def ransortable_attribute?(str, klass) sortable_attributes_for(klass).any? { |s| s.to_sym == str.to_sym } end
def register(&resolver)
Active Record integration (see Ransack::ActiveRecord::Context); an
objects it understands and nil for anything else. Ransack ships the
An ORM integration registers a block that returns a Context for the
def register(&resolver) Ransack::Context.resolvers << resolver end
def resolve_alias_segment(segment)
def resolve_alias_segment(segment) target = ransackable_alias(segment) return target if target != segment path = association_path(segment) return segment if path.blank? remainder = segment.delete_prefix("#{path}_") associated = traverse(path) target = associated._ransack_aliases.fetch(remainder, associated._ransack_aliases.fetch(remainder.to_sym, remainder)) return segment if target == remainder # The target may itself be one attribute whose name contains a # combinator (`terms_and_conditions`); it is prefixed whole. return "#{path}_#{target}" if ransackable_attribute?(target, associated) # Prefix every attribute of the alias's target with the path. target.gsub(/(\A|_and_|_or_)/) { "#{$1}#{path}_" } end
def resolve_aliases(str)
`name_or_email_or_parent_name`. Returns the name unchanged when it
`term_or_daddy` with `term => name_or_email` becomes
compound is joined with that compound's own combinator, so
(`person_term` when Person aliases `term`). An alias that expands to a
associated model and reached through an association path
`_or_` / `_and_` segment of a compound, and an alias defined on an
Expands every `ransack_alias` in a condition name: the whole name, each
def resolve_aliases(str) return str.to_s unless key_within_depth_limit?(str) whole = resolve_alias_segment(str) return whole if whole != str # A name that is an attribute in its own right is never split, even if # it contains a combinator (a column called `true_or_false`). return str if attribute_method?(str) segments = str.split(/_and_|_or_/) resolved = segments.map { |segment| resolve_alias_segment(segment) } return str if resolved == segments combinator = str[/_(or|and)_/, 1] || Constants::OR resolved.join("_#{combinator}_") end
def resolvers
def resolvers @resolvers ||= [] end
def sanitize_scope_args(key, args)
def sanitize_scope_args(key, args) if Ransack.options[:sanitize_scope_args] && !ransackable_scope_skip_sanitize_args?(key, object) cast_scope_args(args) else args end end
def scope_arity(scope)
def scope_arity(scope) @klass.method(scope).arity end
def searchable_associations(str = ''.freeze)
def searchable_associations(str = ''.freeze) associations_for(traverse(str)) end
def searchable_attributes(str = ''.freeze)
def searchable_attributes(str = ''.freeze) attributes_for(traverse(str)) end
def searchable_class?(klass)
Whether a class can be the base of a search. An ORM integration refines
def searchable_class?(klass) klass.respond_to?(:ransackable_attributes) end
def sortable_attributes(str = ''.freeze)
def sortable_attributes(str = ''.freeze) sortable_attributes_for(traverse(str)) end
def sortable_attributes_for(klass)
`ransortable_attributes` defaults to `ransackable_attributes`, so a
def sortable_attributes_for(klass) allowlist(klass, :ransortable_attributes, :ransackable_attributes) do klass.authorizable_ransackable_attributes end end
def strict_conditions?
Unknown attributes, predicates and combinators raise rather than being
def strict_conditions? !Ransack.options[:ignore_unknown_conditions] || ignore_unknown_conditions == false end
def traverse(str, base = @base)
def traverse(str, base = @base) str ||= ''.freeze segments = str.split(Constants::UNDERSCORE) unless segments.empty? remainder = [] found_assoc = nil until found_assoc || segments.empty? # Strip the _of_Model_type text from the association name, but hold # onto it in klass, for use as the next base assoc, klass = unpolymorphize_association( segments.join(Constants::UNDERSCORE) ) if found_assoc = get_association(assoc, base) base = traverse( remainder.join(Constants::UNDERSCORE), klass || found_assoc.klass ) end remainder.unshift segments.pop end unless found_assoc raise(UntraversableAssociationError, "No association matches #{str}") end end klassify(base) end
def unpolymorphize_association(str)
def unpolymorphize_association(str) if (match = str.match(/_of_([^_]+?)_type$/)) && (klass = polymorphic_class(match.captures.first)) [match.pre_match, klass] else [str, nil] end end