mongoid 9.0.10 → 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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -3
  3. data/Rakefile +31 -0
  4. data/lib/config/locales/en.yml +45 -0
  5. data/lib/mongoid/association/depending.rb +8 -4
  6. data/lib/mongoid/association/nested/many.rb +38 -3
  7. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  8. data/lib/mongoid/association/referenced/auto_save.rb +48 -6
  9. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  10. data/lib/mongoid/association/relatable.rb +17 -0
  11. data/lib/mongoid/collection_configurable.rb +8 -0
  12. data/lib/mongoid/config/defaults.rb +9 -5
  13. data/lib/mongoid/config/encryption.rb +36 -18
  14. data/lib/mongoid/config.rb +72 -0
  15. data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
  16. data/lib/mongoid/contextual/memory.rb +18 -7
  17. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  18. data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
  19. data/lib/mongoid/encryptable.rb +46 -0
  20. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  21. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  22. data/lib/mongoid/errors.rb +2 -0
  23. data/lib/mongoid/field_readable.rb +70 -0
  24. data/lib/mongoid/indexable.rb +39 -27
  25. data/lib/mongoid/matchable.rb +6 -1
  26. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +4 -9
  27. data/lib/mongoid/matcher/regex.rb +9 -13
  28. data/lib/mongoid/matcher/regexp_budget.rb +383 -0
  29. data/lib/mongoid/matcher.rb +1 -0
  30. data/lib/mongoid/persistence_context.rb +35 -0
  31. data/lib/mongoid/search_indexable.rb +14 -7
  32. data/lib/mongoid/tasks/database.rb +23 -9
  33. data/lib/mongoid/threaded.rb +37 -0
  34. data/lib/mongoid/version.rb +1 -1
  35. data/lib/mongoid/warnings.rb +4 -0
  36. data/spec/integration/app_spec.rb +201 -36
  37. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  38. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  39. data/spec/integration/encryption_spec.rb +149 -0
  40. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  41. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  42. data/spec/integration/query_operator_guard_spec.rb +89 -0
  43. data/spec/mongoid/association/auto_save_spec.rb +98 -0
  44. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  45. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  46. data/spec/mongoid/attributes/nested_spec.rb +250 -30
  47. data/spec/mongoid/config/defaults_spec.rb +18 -0
  48. data/spec/mongoid/config/encryption_spec.rb +228 -0
  49. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  50. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  51. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
  52. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +200 -0
  53. data/spec/mongoid/criteria_spec.rb +2 -0
  54. data/spec/mongoid/encryptable_spec.rb +61 -0
  55. data/spec/mongoid/errors/document_not_found_spec.rb +10 -3
  56. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  57. data/spec/mongoid/tasks/database_spec.rb +18 -0
  58. data/spec/support/crypt/models.rb +157 -0
  59. metadata +28 -2
@@ -110,6 +110,33 @@ module Mongoid
110
110
  # to `:global_thread_pool`.
111
111
  option :global_executor_concurrency, default: nil
112
112
 
113
+ # When this flag is true, it will be possible to add a record to a
114
+ # "has_many" or "has_and_belongs_to_many" association by passing that
115
+ # record's id in the nested attributes for another parent record, even
116
+ # when the record does not already belong to that association. For a
117
+ # "has_many" association this moves the record to the new parent.
118
+ #
119
+ # When this flag is false, an id in nested attributes is only resolved
120
+ # within the association itself, and anything else raises an error.
121
+ #
122
+ # The default is `false`. Note that allowing reparenting via nested attributes
123
+ # is a potential security risk, since it could allow a malicious user to move
124
+ # records that they do not own to a parent record that they do own.
125
+ #
126
+ # This option will be removed in Mongoid 10, and the only behavior will be
127
+ # as if this option were set to false.
128
+ option :allow_reparenting_via_nested_attributes, default: false
129
+
130
+ # When this flag is true, any documents in associations with `autosave: true`
131
+ # will be saved even if they have not been changed. When this flag is false,
132
+ # only autosaved documents that have been changed will be saved. The default
133
+ # is `true`.
134
+ #
135
+ # This option will default to `false` in Mongoid 9.1, and will be removed
136
+ # in Mongoid 10, with the only behavior at that point being as if this
137
+ # option were set to `false`.
138
+ option :autosave_saves_unchanged_documents, default: true
139
+
113
140
  # When this flag is false, a document will become read-only only once the
114
141
  # #readonly! method is called, and an error will be raised on attempting
115
142
  # to save or update such documents, instead of just on delete. When this
@@ -180,6 +207,51 @@ module Mongoid
180
207
  # See https://jira.mongodb.org/browse/MONGOID-5785 for more details.
181
208
  option :allow_scopes_to_unset_default_scope, default: false
182
209
 
210
+ # The maximum number of seconds that evaluating a single query in memory
211
+ # may spend executing regular expressions. Queries against an embedded
212
+ # association are evaluated in the calling thread, so a pattern built from
213
+ # user input runs locally and can otherwise consume unbounded CPU. The
214
+ # limit is cumulative over the whole query, since cost grows with the
215
+ # number of documents and conditions as well as with the pattern.
216
+ #
217
+ # Set to nil to remove the limit. On Ruby 3.2 and later the remaining
218
+ # budget is compiled into the pattern, so the limit counts only the time
219
+ # spent matching. Earlier Rubies have no per-Regexp timeout, so the query
220
+ # is bounded with Timeout instead and the limit is wall clock over the
221
+ # whole in-memory evaluation.
222
+ #
223
+ # See https://jira.mongodb.org/browse/MONGOID-5981 for details.
224
+ option :in_memory_regexp_time_limit, default: 5.0
225
+
226
+ # When false (default), query operators are restricted when building a
227
+ # selector, so that user-supplied input reaching the query builder cannot
228
+ # make MongoDB execute arbitrary JavaScript. Two rules are enforced:
229
+ #
230
+ # - An operator at the top level of an expression must appear in
231
+ # +Criteria::Queryable::Selectable::ALLOWED_QUERY_OPERATORS+.
232
+ # - +$where+, +$function+, and +$accumulator+ are rejected at any depth,
233
+ # including inside +$expr+ and the logical operators.
234
+ #
235
+ # This applies to every query method that accepts an expression, including
236
+ # +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
237
+ # It also governs the string form of +where+, e.g.
238
+ # +where("this.name == 'admin'")+, which the server evaluates as +$where+.
239
+ # Applications relying on that form must set this option to true.
240
+ #
241
+ # The APIs that request JavaScript explicitly, +Criteria#for_js+ and
242
+ # +js_query+, are unaffected: there the developer has asked for it.
243
+ #
244
+ # Set to true to restore the unrestricted pass-through behavior.
245
+ #
246
+ # Note that this option is deliberately not tied to +load_defaults+: an
247
+ # application that has opted into older defaults still gets the guard, and
248
+ # must set this option explicitly to turn it off.
249
+ #
250
+ # See https://jira.mongodb.org/browse/MONGOID-5939,
251
+ # https://jira.mongodb.org/browse/MONGOID-5993,
252
+ # https://jira.mongodb.org/browse/MONGOID-5994 for details.
253
+ option :allow_unsafe_query_operators, default: false
254
+
183
255
  # Returns the Config singleton, for use in the configure DSL.
184
256
  #
185
257
  # @return [ self ] The Config singleton.
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
  # rubocop:todo all
3
3
 
4
+ require 'mongoid/field_readable'
5
+
4
6
  module Mongoid
5
7
  module Contextual
6
8
  module Aggregable
7
9
  # Contains behavior for aggregating values in memory.
8
10
  module Memory
11
+ include FieldReadable
9
12
 
10
13
  # Get all the aggregate values for the provided field.
11
14
  # Provided for interface consistency with Aggregable::Mongo.
@@ -30,7 +33,7 @@ module Mongoid
30
33
  #
31
34
  # @return [ Numeric ] The average.
32
35
  def avg(field)
33
- total = count { |doc| !doc.send(field).nil? }
36
+ total = count { |doc| !read_field_value(doc, field).nil? }
34
37
  return nil unless total > 0
35
38
 
36
39
  total = total.to_f if total.is_a?(Integer)
@@ -116,7 +119,7 @@ module Mongoid
116
119
  def aggregate_by(field, method)
117
120
  return nil unless any?
118
121
 
119
- map { |doc| doc.public_send(field) }.compact.public_send(method)
122
+ map { |doc| read_field_value(doc, field) }.compact.public_send(method)
120
123
  end
121
124
  end
122
125
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  require "mongoid/contextual/aggregable/memory"
5
5
  require "mongoid/association/eager_loadable"
6
+ require "mongoid/field_readable"
6
7
 
7
8
  module Mongoid
8
9
  module Contextual
@@ -17,6 +18,7 @@ module Mongoid
17
18
  include Association::EagerLoadable
18
19
  include Queryable
19
20
  include Positional
21
+ include FieldReadable
20
22
 
21
23
  # @attribute [r] root The root document.
22
24
  # @attribute [r] path The atomic path.
@@ -172,10 +174,20 @@ module Mongoid
172
174
  # @param [ Criteria ] criteria The criteria.
173
175
  def initialize(criteria)
174
176
  @criteria, @klass = criteria, criteria.klass
175
- @documents = criteria.documents.select do |doc|
176
- @root ||= doc._root
177
- @collection ||= root.collection
178
- doc._matches?(criteria.selector)
177
+
178
+ # Resolving the collection can build a Mongo::Client the first time it
179
+ # runs, so it happens before the budget opens. Where the budget is
180
+ # enforced with Timeout the exception is asynchronous, and landing in
181
+ # the middle of that would leave a half-built client behind.
182
+ if (first = criteria.documents.first)
183
+ @root = first._root
184
+ @collection = @root.collection
185
+ end
186
+
187
+ # One regexp budget covers the whole scan, so that the limit bounds the
188
+ # query rather than each document individually.
189
+ @documents = Matcher::RegexpBudget.open(criteria.selector) do
190
+ criteria.documents.select { |doc| doc._matches?(criteria.selector) }
179
191
  end
180
192
  apply_sorting
181
193
  apply_options
@@ -733,11 +745,10 @@ module Mongoid
733
745
  # _translations hash so that we can get the specified translation in
734
746
  # the remaining
735
747
  if field&.localized?
736
- document.send("#{segment}_translations")
748
+ document.public_send("#{segment}_translations")
737
749
  end
738
750
  end
739
- meth = klass.aliased_associations[segment] || segment
740
- res.nil? ? document.try(meth) : res
751
+ res.nil? ? read_field_value(document, segment) : res
741
752
  elsif document.is_a?(Hash)
742
753
  # TODO: Remove the indifferent access when implementing MONGOID-5410.
743
754
  document.key?(segment.to_s) ?
@@ -326,6 +326,10 @@ module Mongoid
326
326
  end
327
327
  end
328
328
  end
329
+ # Every query method that takes a user-supplied expression normalizes
330
+ # it here, so this is where the operator guard is enforced. See
331
+ # Selectable#_mongoid_validate_operators! for what it does not cover.
332
+ _mongoid_validate_operators!(result)
329
333
  result
330
334
  end
331
335
 
@@ -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