mongoid 9.0.11 → 9.0.12

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +45 -0
  3. data/lib/mongoid/association/depending.rb +8 -4
  4. data/lib/mongoid/association/nested/many.rb +34 -5
  5. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  7. data/lib/mongoid/association/relatable.rb +17 -0
  8. data/lib/mongoid/collection_configurable.rb +8 -0
  9. data/lib/mongoid/config/encryption.rb +36 -18
  10. data/lib/mongoid/config.rb +56 -9
  11. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  12. data/lib/mongoid/contextual/memory.rb +18 -7
  13. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  14. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  15. data/lib/mongoid/encryptable.rb +46 -0
  16. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  17. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  18. data/lib/mongoid/errors.rb +2 -0
  19. data/lib/mongoid/field_readable.rb +70 -0
  20. data/lib/mongoid/indexable.rb +39 -27
  21. data/lib/mongoid/matchable.rb +6 -1
  22. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  23. data/lib/mongoid/matcher/regex.rb +9 -13
  24. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  25. data/lib/mongoid/matcher.rb +1 -0
  26. data/lib/mongoid/persistence_context.rb +35 -0
  27. data/lib/mongoid/search_indexable.rb +14 -7
  28. data/lib/mongoid/tasks/database.rb +23 -9
  29. data/lib/mongoid/threaded.rb +37 -0
  30. data/lib/mongoid/version.rb +1 -1
  31. data/spec/integration/app_spec.rb +7 -0
  32. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  33. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  34. data/spec/integration/encryption_spec.rb +149 -0
  35. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  36. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  37. data/spec/integration/query_operator_guard_spec.rb +89 -0
  38. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  39. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  40. data/spec/mongoid/attributes/nested_spec.rb +204 -10
  41. data/spec/mongoid/config/defaults_spec.rb +18 -0
  42. data/spec/mongoid/config/encryption_spec.rb +228 -0
  43. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  44. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  45. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  46. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  47. data/spec/mongoid/criteria_spec.rb +2 -0
  48. data/spec/mongoid/encryptable_spec.rb +61 -0
  49. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  50. data/spec/mongoid/tasks/database_spec.rb +18 -0
  51. data/spec/support/crypt/models.rb +157 -0
  52. metadata +14 -2
@@ -823,6 +823,12 @@ module Mongoid
823
823
  # only ever specify one criterion to #where.
824
824
  @criterion = criterion
825
825
  if criterion.is_a?(String)
826
+ unless Mongoid.allow_unsafe_query_operators?
827
+ raise Errors::InvalidQuery,
828
+ "String criteria are not allowed because they compile to the '$where' operator, " \
829
+ 'which is not allowed in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
830
+ 'to permit all operators.'
831
+ end
826
832
  js_query(criterion)
827
833
  else
828
834
  expr_query(criterion)
@@ -832,6 +838,19 @@ module Mongoid
832
838
 
833
839
  private
834
840
 
841
+ # Operators permitted at the top level of a query expression without
842
+ # opt-in. Excludes $where (JS execution) and other operators not needed
843
+ # for ordinary application queries.
844
+ ALLOWED_QUERY_OPERATORS = %w[
845
+ $and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue
846
+ ].freeze
847
+
848
+ # Operators that execute server-side JavaScript. These are rejected at
849
+ # any depth, not just at the top level: the allowlist above permits
850
+ # $expr and the logical operators, and their values are arbitrary
851
+ # nested expressions that can carry $function or $where.
852
+ JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
853
+
835
854
  # Adds the specified expression to the query.
836
855
  #
837
856
  # Criterion must be a hash in one of the following forms:
@@ -857,6 +876,8 @@ module Mongoid
857
876
  raise Errors::InvalidQuery, "Expression must be a Hash: #{Errors::InvalidQuery.truncate_expr(criterion)}"
858
877
  end
859
878
 
879
+ # The operator guard is applied by _mongoid_expand_keys, which every
880
+ # query method that accepts a user-supplied expression passes through.
860
881
  normalized = _mongoid_expand_keys(criterion)
861
882
  clone.tap do |query|
862
883
  normalized.each do |field, value|
@@ -872,6 +893,94 @@ module Mongoid
872
893
  end
873
894
  end
874
895
 
896
+ # Enforces the operator rules governed by the
897
+ # +allow_unsafe_query_operators+ configuration option against a
898
+ # normalized query expression.
899
+ #
900
+ # Two rules apply, and both are skipped when the option is true:
901
+ #
902
+ # - An operator at the top level of the expression must appear in
903
+ # ALLOWED_QUERY_OPERATORS.
904
+ # - An operator in JAVASCRIPT_QUERY_OPERATORS is rejected at any depth.
905
+ #
906
+ # This is called from #_mongoid_expand_keys rather than from the
907
+ # individual query methods, because that is the one point every query
908
+ # method taking a user-supplied expression passes through on its way to
909
+ # the selector.
910
+ #
911
+ # It deliberately does not cover the APIs that ask for JavaScript
912
+ # outright, such as #js_query and Criteria#for_js: there the developer
913
+ # has chosen server-side JavaScript, so there is nothing to guard
914
+ # against. The same goes for the low-level Storable methods
915
+ # (#add_field_expression, #add_operator_expression), which write to the
916
+ # selector directly.
917
+ #
918
+ # @param [ Hash ] expr A normalized query expression.
919
+ #
920
+ # @raise [ Errors::InvalidQuery ] If a disallowed operator is present.
921
+ #
922
+ # @api private
923
+ def _mongoid_validate_operators!(expr)
924
+ return if Mongoid.allow_unsafe_query_operators?
925
+
926
+ expr.each_key do |field|
927
+ field_s = field.to_s
928
+ next unless field_s.start_with?('$')
929
+ next if ALLOWED_QUERY_OPERATORS.include?(field_s)
930
+
931
+ raise Errors::InvalidQuery,
932
+ "Operator '#{field_s}' is not allowed in a query expression. " \
933
+ 'Set Mongoid.allow_unsafe_query_operators = true to permit all operators.'
934
+ end
935
+
936
+ _mongoid_validate_no_javascript!(expr)
937
+ end
938
+
939
+ # Walks a query expression looking for operators that execute
940
+ # server-side JavaScript, descending through both hashes and arrays so
941
+ # that nested forms such as {'$expr' => {'$function' => ...}} and
942
+ # {'$or' => [ {'$where' => ...} ]} are caught.
943
+ #
944
+ # The walk does not distinguish operator position from value position,
945
+ # so it also rejects queries where a JavaScript operator name appears as
946
+ # data rather than as an operator. Server 5.0+ permits $-prefixed field
947
+ # names in stored documents, which makes this reachable:
948
+ #
949
+ # Doc.where(payload: { '$eq' => { '$function' => 'abc' } })
950
+ #
951
+ # Here the $eq marks its argument as a literal value to compare, so the
952
+ # server never evaluates it, but the guard raises anyway. The only
953
+ # workaround today is the global allow_unsafe_query_operators flag.
954
+ #
955
+ # TODO: discuss whether to track operator position (skipping the subtree
956
+ # under $eq, $ne, $in, $nin, and $elemMatch values) in a future
957
+ # iteration. It removes the false positive but adds exactly the kind of
958
+ # state that a real bypass could hide in, so it was left out for now.
959
+ #
960
+ # @param [ Object ] object A fragment of a query expression.
961
+ #
962
+ # @raise [ Errors::InvalidQuery ] If a JavaScript operator is present.
963
+ #
964
+ # @api private
965
+ def _mongoid_validate_no_javascript!(object)
966
+ case object
967
+ when Hash
968
+ object.each do |key, value|
969
+ key_s = key.to_s
970
+ if JAVASCRIPT_QUERY_OPERATORS.include?(key_s)
971
+ raise Errors::InvalidQuery,
972
+ "Operator '#{key_s}' executes server-side JavaScript and is not allowed " \
973
+ 'anywhere in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
974
+ 'to permit all operators.'
975
+ end
976
+
977
+ _mongoid_validate_no_javascript!(value)
978
+ end
979
+ when Array
980
+ object.each { |value| _mongoid_validate_no_javascript!(value) }
981
+ end
982
+ end
983
+
875
984
  # Force the values of the criterion to be evolved.
876
985
  #
877
986
  # @api private
@@ -37,6 +37,52 @@ module Mongoid
37
37
  !encrypt_metadata.empty? || fields.any? { |_, field| field.is_a?(Mongoid::Fields::Encrypted) }
38
38
  end
39
39
 
40
+ # Whether an encryption schema has to be generated for this model.
41
+ #
42
+ # True when the model declares encryption itself, and also when any model
43
+ # reachable through its embeds_one relations does. A model in the second
44
+ # group has no encrypted field of its own, but its collection still needs
45
+ # a schema, otherwise the embedded fields are written in plaintext.
46
+ #
47
+ # The answer is memoized, since this runs on the persistence path.
48
+ # Declaring encryption on a model after it has already been persisted is
49
+ # not supported.
50
+ #
51
+ # @return [ true | false ] Whether the model needs an encryption schema.
52
+ #
53
+ # @api private
54
+ def requires_encryption_schema?
55
+ return @requires_encryption_schema if defined?(@requires_encryption_schema)
56
+
57
+ @requires_encryption_schema = encrypted? || embeds_encrypted?([ self ])
58
+ end
59
+
60
+ # Whether any model reachable through this model's embeds_one relations
61
+ # declares encryption.
62
+ #
63
+ # embeds_many is not considered: libmongocrypt cannot express per-field
64
+ # encryption under array items, so those fields are never mapped.
65
+ #
66
+ # @param [ Array<Class> ] path The models the walk is already inside of.
67
+ # A model embedding itself terminates here.
68
+ #
69
+ # @return [ true | false ] Whether an embedded model is encrypted.
70
+ #
71
+ # @api private
72
+ def embeds_encrypted?(path)
73
+ relations.each_value.any? do |relation|
74
+ next false unless relation.is_a?(Association::Embedded::EmbedsOne)
75
+
76
+ klass = relation.try_relation_class
77
+ # An association target does not have to be a Mongoid document, and
78
+ # the class it names does not have to exist.
79
+ next false unless klass.respond_to?(:encrypted?)
80
+ next false if path.include?(klass)
81
+
82
+ klass.encrypted? || klass.embeds_encrypted?(path + [ klass ])
83
+ end
84
+ end
85
+
40
86
  # Override the key_id for the model.
41
87
  #
42
88
  # This method is solely for testing purposes and should not be used in
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # This error is raised when evaluating a query in memory exceeds
6
+ # Mongoid::Config.in_memory_regexp_time_limit.
7
+ #
8
+ # What the limit bounds depends on the Ruby in use. Where per-Regexp
9
+ # timeouts are available it is the time spent executing regular
10
+ # expressions; elsewhere it is the elapsed time of the whole in-memory
11
+ # evaluation. The message is worded to hold either way.
12
+ class InMemoryRegexpTimeout < MongoidError
13
+ # Create the new error.
14
+ #
15
+ # @example Create the new in-memory regexp timeout error.
16
+ # InMemoryRegexpTimeout.new(5.0)
17
+ #
18
+ # @param [ Float ] limit The limit that was exceeded, in seconds. Not
19
+ # always the configured one: a global Regexp.timeout stricter than the
20
+ # configuration takes its place.
21
+ def initialize(limit)
22
+ super(compose_message('in_memory_regexp_timeout', limit: limit))
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mongoid
4
+ module Errors
5
+ # This error is raised when a model declares encrypted fields, but the
6
+ # namespace it is about to be persisted to is not covered by the automatic
7
+ # encryption schema of the client in use. Without a schema the driver has
8
+ # nothing to encrypt with, and the fields would be stored in plaintext.
9
+ class NoEncryptionSchema < MongoidError
10
+ # Create the new error.
11
+ #
12
+ # @example Create the error.
13
+ # NoEncryptionSchema.new(Band, 'music.bands', :default)
14
+ #
15
+ # @param [ Class ] klass The model class.
16
+ # @param [ String ] namespace The namespace the model resolved to.
17
+ # @param [ String | Symbol ] client The name of the client in use.
18
+ def initialize(klass, namespace, client)
19
+ super(
20
+ compose_message(
21
+ 'no_encryption_schema',
22
+ { klass: klass, namespace: namespace, client: client }
23
+ )
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -12,6 +12,7 @@ require "mongoid/errors/document_not_found"
12
12
  require "mongoid/errors/empty_config_file"
13
13
  require "mongoid/errors/immutable_attribute"
14
14
  require "mongoid/errors/in_memory_collation_not_supported"
15
+ require "mongoid/errors/in_memory_regexp_timeout"
15
16
  require "mongoid/errors/invalid_auto_encryption_configuration"
16
17
  require "mongoid/errors/invalid_around_callback"
17
18
  require "mongoid/errors/invalid_async_query_executor"
@@ -52,6 +53,7 @@ require "mongoid/errors/mixed_client_configuration"
52
53
  require "mongoid/errors/nested_attributes_metadata_not_found"
53
54
  require "mongoid/errors/no_default_client"
54
55
  require "mongoid/errors/no_environment"
56
+ require "mongoid/errors/no_encryption_schema"
55
57
  require "mongoid/errors/no_map_reduce_output"
56
58
  require "mongoid/errors/no_metadata"
57
59
  require "mongoid/errors/no_parent"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ # rubocop:todo all
3
+
4
+ module Mongoid
5
+ # Reads the value of a field name from a document without dispatching the
6
+ # name as an arbitrary method.
7
+ #
8
+ # Field names may come from application input, where a name like +destroy+
9
+ # or +attributes+ would delete the document or disclose its contents.
10
+ # Declared field and association names are validated when they are declared
11
+ # (see Mongoid.destructive_fields), so a name that resolves to one of them
12
+ # is safe to send to a document. Any other name is read from the attributes
13
+ # hash, which is what the database-backed query contexts do.
14
+ #
15
+ # @api private
16
+ module FieldReadable
17
+ private
18
+
19
+ # Read the value of the given field name from the given document.
20
+ #
21
+ # @param [ Document ] document The document to read from.
22
+ # @param [ String | Symbol ] name The name of the field.
23
+ #
24
+ # @return [ Object | nil ] The value of the field, or nil when the name
25
+ # is neither a declared field nor present in the attributes.
26
+ def read_field_value(document, name)
27
+ name = name.to_s
28
+ # A blank name cannot name a field. Return nil, which is what reading
29
+ # one has always done.
30
+ return nil if name.blank?
31
+
32
+ if (meth = readable_method_for(document.class, name))
33
+ document.public_send(meth)
34
+ else
35
+ document.attributes[document.class.database_field_name(name)]
36
+ end
37
+ end
38
+
39
+ # Resolve the given name to a method that is declared by the given class,
40
+ # and therefore safe to send to one of its instances.
41
+ #
42
+ # @param [ Class ] klass The document class.
43
+ # @param [ String ] name The name of the field.
44
+ #
45
+ # @return [ String | nil ] The method to send, or nil when the name is
46
+ # not declared by the class.
47
+ def readable_method_for(klass, name)
48
+ # Fields, associations, and field aliases each define a reader of their
49
+ # own name. Note that associations must be resolved before aliases: a
50
+ # belongs_to aliases its own name to its foreign key, and reading
51
+ # `band` must give the document, not the id.
52
+ return name if klass.relations.key?(name) ||
53
+ klass.fields.key?(name) ||
54
+ klass.aliased_fields.key?(name)
55
+
56
+ # An association may also be named by its `store_as`, which has no
57
+ # reader of its own, or by its ids accessor, which does.
58
+ if (assoc = klass.relations[klass.aliased_associations[name]])
59
+ return (assoc.store_as == name) ? assoc.name.to_s : name
60
+ end
61
+
62
+ # Localized fields also get a _translations reader, which does not
63
+ # appear in the fields hash.
64
+ base = name.delete_suffix(Fields::TRANSLATIONS_SFX)
65
+ return nil if base == name
66
+
67
+ name if klass.fields[klass.database_field_name(base)]&.localized?
68
+ end
69
+ end
70
+ end
@@ -27,18 +27,9 @@ module Mongoid
27
27
  def create_indexes
28
28
  return unless index_specifications
29
29
 
30
- default_options = {background: Config.background_indexing}
31
-
32
- index_specifications.each do |spec|
33
- key, options = spec.key, default_options.merge(spec.options)
34
- if database = options[:database]
35
- with(database: database) do |klass|
36
- klass.collection.indexes(session: _session).create_one(key, options.except(:database))
37
- end
38
- else
39
- collection.indexes(session: _session).create_one(key, options)
40
- end
41
- end and true
30
+ Threaded.with_collection_management do
31
+ perform_create_indexes
32
+ end
42
33
  end
43
34
 
44
35
  # Send the actual index removal comments to the MongoDB driver,
@@ -49,21 +40,9 @@ module Mongoid
49
40
  #
50
41
  # @return [ true ] If the operation succeeded.
51
42
  def remove_indexes
52
- indexed_database_names.each do |database|
53
- with(database: database) do |klass|
54
- begin
55
- klass.collection.indexes(session: _session).each do |spec|
56
- unless spec["name"] == "_id_"
57
- klass.collection.indexes(session: _session).drop_one(spec["key"])
58
- logger.info(
59
- "MONGOID: Removed index '#{spec["name"]}' on collection " +
60
- "'#{klass.collection.name}' in database '#{database}'."
61
- )
62
- end
63
- end
64
- rescue Mongo::Error::OperationFailure; end
65
- end
66
- end and true
43
+ Threaded.with_collection_management do
44
+ perform_remove_indexes
45
+ end
67
46
  end
68
47
 
69
48
  # Add the default indexes to the root document if they do not already
@@ -118,6 +97,39 @@ module Mongoid
118
97
 
119
98
  private
120
99
 
100
+ def perform_create_indexes
101
+ default_options = {background: Config.background_indexing}
102
+
103
+ index_specifications.each do |spec|
104
+ key, options = spec.key, default_options.merge(spec.options)
105
+ if database = options[:database]
106
+ with(database: database) do |klass|
107
+ klass.collection.indexes(session: _session).create_one(key, options.except(:database))
108
+ end
109
+ else
110
+ collection.indexes(session: _session).create_one(key, options)
111
+ end
112
+ end and true
113
+ end
114
+
115
+ def perform_remove_indexes
116
+ indexed_database_names.each do |database|
117
+ with(database: database) do |klass|
118
+ begin
119
+ klass.collection.indexes(session: _session).each do |spec|
120
+ unless spec["name"] == "_id_"
121
+ klass.collection.indexes(session: _session).drop_one(spec["key"])
122
+ logger.info(
123
+ "MONGOID: Removed index '#{spec["name"]}' on collection " +
124
+ "'#{klass.collection.name}' in database '#{database}'."
125
+ )
126
+ end
127
+ end
128
+ rescue Mongo::Error::OperationFailure; end
129
+ end
130
+ end and true
131
+ end
132
+
121
133
  # Get the names of all databases for this model that have index
122
134
  # definitions.
123
135
  #
@@ -18,7 +18,12 @@ module Mongoid
18
18
  #
19
19
  # @return [ true | false ] True if matches, false if not.
20
20
  def _matches?(selector)
21
- Matcher::Expression.matches?(self, selector)
21
+ # Opens a regexp budget for this document only. Callers that match many
22
+ # documents against one selector open a budget of their own first, and
23
+ # this one joins it rather than giving every document a fresh limit.
24
+ Matcher::RegexpBudget.open(selector) do
25
+ Matcher::Expression.matches?(self, selector)
26
+ end
22
27
  end
23
28
  end
24
29
  end
@@ -1,13 +1,10 @@
1
- # rubocop:todo all
2
1
  module Mongoid
3
2
  module Matcher
4
-
5
3
  # This is an internal equality implementation that performs exact
6
4
  # comparisons and regular expression matches.
7
5
  #
8
6
  # @api private
9
7
  module EqImplWithRegexp
10
-
11
8
  # Returns whether a value satisfies an $eq (or similar) expression,
12
9
  # performing a regular expression match if the condition is a regular
13
10
  # expression.
@@ -19,14 +16,12 @@ module Mongoid
19
16
  # @return [ true | false ] Whether the value matches.
20
17
  #
21
18
  # @api private
22
- module_function def matches?(original_operator, value, condition)
19
+ module_function def matches?(_original_operator, value, condition)
23
20
  case condition
24
- when Regexp
25
- value =~ condition
26
- when ::BSON::Regexp::Raw
27
- value =~ condition.compile
21
+ when Regexp, ::BSON::Regexp::Raw
22
+ value.respond_to?(:=~) && RegexpBudget.match?(value, condition)
28
23
  else
29
- if value.kind_of?(Time) && condition.kind_of?(Time)
24
+ if value.is_a?(Time) && condition.is_a?(Time)
30
25
  EqImpl.time_eq?(value, condition)
31
26
  else
32
27
  value == condition
@@ -1,14 +1,11 @@
1
- # rubocop:todo all
2
1
  module Mongoid
3
2
  module Matcher
4
-
5
3
  # In-memory matcher for $regex expression.
6
4
  #
7
5
  # @see https://www.mongodb.com/docs/manual/reference/operator/query/regex/
8
6
  #
9
7
  # @api private
10
8
  module Regex
11
-
12
9
  # Returns whether a value satisfies a $regex expression.
13
10
  #
14
11
  # @param [ true | false ] exists Not used.
@@ -18,25 +15,24 @@ module Mongoid
18
15
  # @return [ true | false ] Whether the value matches.
19
16
  #
20
17
  # @api private
21
- module_function def matches?(exists, value, condition)
22
- condition = case condition
23
- when Regexp
24
- condition
25
- when BSON::Regexp::Raw
26
- condition.compile
27
- else
18
+ module_function def matches?(_exists, value, condition)
19
+ unless condition.is_a?(Regexp) || condition.is_a?(BSON::Regexp::Raw)
28
20
  # Note that strings must have been converted to a regular expression
29
21
  # instance already (with $options taken into account, if provided).
30
22
  raise Errors::InvalidQuery, "$regex requires a regular expression argument: #{Errors::InvalidQuery.truncate_expr(condition)}"
31
23
  end
32
24
 
25
+ # The condition is compiled by RegexpBudget rather than here, so that
26
+ # the budget's timeout can be baked into the pattern.
33
27
  case value
34
28
  when Array
29
+ # Object#=~ is gone as of Ruby 3.2, so an element that cannot be
30
+ # matched against has to be rejected rather than passed to =~.
35
31
  value.any? do |v|
36
- v =~ condition
32
+ v.respond_to?(:=~) && RegexpBudget.match?(v, condition)
37
33
  end
38
34
  when String
39
- value =~ condition
35
+ RegexpBudget.match?(value, condition)
40
36
  else
41
37
  false
42
38
  end
@@ -52,7 +48,7 @@ module Mongoid
52
48
  #
53
49
  # @api private
54
50
  module_function def matches_array_or_scalar?(value, condition)
55
- if Array === value
51
+ if value.is_a?(Array)
56
52
  value.any? do |v|
57
53
  matches?(true, v, condition)
58
54
  end