mongoid 8.1.12 → 8.1.13
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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +26 -0
- data/lib/mongoid/association/depending.rb +8 -4
- data/lib/mongoid/association/nested/many.rb +38 -3
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/config.rb +62 -0
- data/lib/mongoid/contextual/aggregable/memory.rb +5 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/queryable/mergeable.rb +12 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +109 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/field_readable.rb +70 -0
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +3 -5
- data/lib/mongoid/matcher/regex.rb +8 -9
- data/lib/mongoid/matcher/regexp_budget.rb +389 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/threaded.rb +3 -0
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +1 -0
- data/spec/integration/app_spec.rb +8 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/attributes/nested_spec.rb +204 -10
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +2 -0
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +235 -0
- data/spec/mongoid/criteria_spec.rb +2 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- metadata +11 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 519052cca069b6c2b2c3aa7cf6fc561b275b8df20824d3b587665ff64e9823d7
|
|
4
|
+
data.tar.gz: 130d16306e79e262499c1faa2320c3adb0dabb5874b1d3053a950a3026324352
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 37f2ee4e9b4f45e56c4c2552ab522b0f716548bc114d033a17eab6a84b451c4a9dc2d6c3b1e255b3e44fbbc9800ccfdb4aef5f7f25c4c4839650fd1e16c2c1cd
|
|
7
|
+
data.tar.gz: 57f22359f24b877b4d58ace31759e8e0958dd149a0973d1e47359196e3aa702034582991068df5158b1930c5cb773ac88ec5da45afc44bd85c329461b359fb1b
|
data/lib/config/locales/en.yml
CHANGED
|
@@ -129,6 +129,32 @@ en:
|
|
|
129
129
|
A collation option is only supported if the query is executed on a MongoDB server
|
|
130
130
|
with version >= 3.4."
|
|
131
131
|
resolution: "Remove the collation option from the query."
|
|
132
|
+
in_memory_regexp_timeout:
|
|
133
|
+
message: "Evaluating this query in memory exceeded the %{limit} second
|
|
134
|
+
limit that applies to conditions containing a regular expression."
|
|
135
|
+
summary: "Mongoid evaluates some conditions in the calling thread
|
|
136
|
+
rather than sending them to the server: queries against an embedded
|
|
137
|
+
association, removing documents from an association by condition,
|
|
138
|
+
and Document#_matches?. A regular expression in such a condition
|
|
139
|
+
runs locally. Mongoid limits the total time one in-memory
|
|
140
|
+
evaluation may spend matching, because both the cost of a single
|
|
141
|
+
match and the number of matches performed grow with the condition,
|
|
142
|
+
and a pattern built from end-user input can be made to consume an
|
|
143
|
+
unbounded amount of CPU. The limit is cumulative over the whole
|
|
144
|
+
evaluation rather than per match. Where the Ruby in use offers no
|
|
145
|
+
per-Regexp timeout the limit is enforced as wall clock over the
|
|
146
|
+
whole in-memory evaluation, so there it can be exceeded by a large
|
|
147
|
+
scan even when the pattern itself is cheap."
|
|
148
|
+
resolution: "Check whether the pattern in this query comes from user
|
|
149
|
+
input; if it does, validate it before querying. If the query is
|
|
150
|
+
legitimate and simply large, raise
|
|
151
|
+
Mongoid::Config.in_memory_regexp_time_limit, or set it to nil to
|
|
152
|
+
remove the limit entirely. The limit in force is the smaller of
|
|
153
|
+
that setting and any global Regexp.timeout the application has set,
|
|
154
|
+
so check both. On JRuby, and on MRI before 3.2, there is no
|
|
155
|
+
per-Regexp timeout, so what the limit bounds there is the elapsed
|
|
156
|
+
time of the whole in-memory evaluation rather than the time spent
|
|
157
|
+
matching."
|
|
132
158
|
invalid_async_query_executor:
|
|
133
159
|
message: "Invalid async_query_executor option: %{executor}."
|
|
134
160
|
summary: "A invalid async query executor was specified.
|
|
@@ -11,11 +11,15 @@ module Mongoid
|
|
|
11
11
|
included do
|
|
12
12
|
class_attribute :dependents
|
|
13
13
|
|
|
14
|
+
# Note the leading underscore: without it, ActiveSupport's
|
|
15
|
+
# class_attribute would generate a helper method for this attribute
|
|
16
|
+
# that collides with one it generates for :dependents.
|
|
17
|
+
#
|
|
14
18
|
# @api private
|
|
15
|
-
class_attribute :
|
|
19
|
+
class_attribute :_dependents_owner
|
|
16
20
|
|
|
17
21
|
self.dependents = []
|
|
18
|
-
self.
|
|
22
|
+
self._dependents_owner = self
|
|
19
23
|
end
|
|
20
24
|
|
|
21
25
|
class_methods do
|
|
@@ -50,9 +54,9 @@ module Mongoid
|
|
|
50
54
|
def self.define_dependency!(association)
|
|
51
55
|
validate!(association)
|
|
52
56
|
association.inverse_class.tap do |klass|
|
|
53
|
-
if klass.
|
|
57
|
+
if klass._dependents_owner != klass
|
|
54
58
|
klass.dependents = []
|
|
55
|
-
klass.
|
|
59
|
+
klass._dependents_owner = klass
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
if association.dependent && !klass.dependents.include?(association)
|
|
@@ -169,7 +169,7 @@ module Mongoid
|
|
|
169
169
|
# @param [ Hash ] attrs The single document attributes to process.
|
|
170
170
|
def update_nested_relation(parent, id, attrs)
|
|
171
171
|
first = existing.first
|
|
172
|
-
converted = first ?
|
|
172
|
+
converted = convert_id(first ? first.class : association.klass, id)
|
|
173
173
|
|
|
174
174
|
if existing.where(_id: converted).exists?
|
|
175
175
|
# document exists in association
|
|
@@ -179,11 +179,46 @@ module Mongoid
|
|
|
179
179
|
else
|
|
180
180
|
update_document(doc, attrs)
|
|
181
181
|
end
|
|
182
|
-
|
|
182
|
+
elsif association.embedded?
|
|
183
|
+
raise Errors::DocumentNotFound.new(association.klass, id)
|
|
184
|
+
elsif destroyable?(attrs)
|
|
185
|
+
# A destroy of a document that is not in the association is
|
|
186
|
+
# ignored, rather than reaching for it outside the association.
|
|
187
|
+
nil
|
|
188
|
+
elsif Mongoid.allow_reparenting_via_nested_attributes?
|
|
189
|
+
Mongoid::Warnings.warn_reparenting_via_nested_attributes
|
|
190
|
+
|
|
183
191
|
# push existing document to association
|
|
184
192
|
doc = association.klass.unscoped.find(converted)
|
|
193
|
+
# find returns nil instead of raising when
|
|
194
|
+
# Mongoid.raise_not_found_error is false; a missing document
|
|
195
|
+
# must still be an error here, consistent with the other
|
|
196
|
+
# not-found branches.
|
|
197
|
+
raise Errors::DocumentNotFound.new(association.klass, id) if doc.nil?
|
|
198
|
+
|
|
185
199
|
update_document(doc, attrs)
|
|
186
|
-
existing.push(doc)
|
|
200
|
+
existing.push(doc)
|
|
201
|
+
else
|
|
202
|
+
raise Errors::DocumentNotFound.new(association.klass, not_found_params(parent, id))
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# The params to report when an id in the nested attributes could not
|
|
207
|
+
# be resolved within the association.
|
|
208
|
+
#
|
|
209
|
+
# @api private
|
|
210
|
+
#
|
|
211
|
+
# @param [ Document ] parent The parent document.
|
|
212
|
+
# @param [ String | BSON::ObjectId ] id of the related document.
|
|
213
|
+
#
|
|
214
|
+
# @return [ Hash | Object ] The params for the not found error.
|
|
215
|
+
def not_found_params(parent, id)
|
|
216
|
+
if association.is_a?(Association::Referenced::HasAndBelongsToMany)
|
|
217
|
+
# A many-to-many association has no foreign key on the child, so
|
|
218
|
+
# the id is only ever resolved within the association itself.
|
|
219
|
+
id
|
|
220
|
+
else
|
|
221
|
+
{ _id: id, association.foreign_key => parent.id }
|
|
187
222
|
end
|
|
188
223
|
end
|
|
189
224
|
end
|
|
@@ -53,12 +53,26 @@ module Mongoid
|
|
|
53
53
|
# @example Convert the id.
|
|
54
54
|
# builder.convert_id(Person, "4d371b444835d98b8b000010")
|
|
55
55
|
#
|
|
56
|
+
# Ids arriving from a form are always scalars. A Hash or an Array
|
|
57
|
+
# here means the parameters were crafted, and letting one through
|
|
58
|
+
# would turn the id into a query operator, so they are rejected.
|
|
59
|
+
#
|
|
56
60
|
# @param [ Class ] klass The class we're trying to convert for.
|
|
57
61
|
# @param [ String ] id The id, usually coming from the form.
|
|
58
62
|
#
|
|
59
63
|
# @return [ BSON::ObjectId | String | Object ] The converted id.
|
|
64
|
+
#
|
|
65
|
+
# @raise [ Errors::DocumentNotFound ] if the id is not a scalar, or
|
|
66
|
+
# cannot be converted to the type the class uses for its ids.
|
|
67
|
+
# The BSON::Error rescue is defensive: a value that reaches
|
|
68
|
+
# BSON::ObjectId.mongoize and raises there must not surface as an
|
|
69
|
+
# unhandled BSON error in the caller.
|
|
60
70
|
def convert_id(klass, id)
|
|
71
|
+
raise Errors::DocumentNotFound.new(klass, id) if id.is_a?(::Hash) || id.is_a?(::Array)
|
|
72
|
+
|
|
61
73
|
klass.using_object_ids? ? BSON::ObjectId.mongoize(id) : id
|
|
74
|
+
rescue BSON::Error
|
|
75
|
+
raise Errors::DocumentNotFound.new(klass, id)
|
|
62
76
|
end
|
|
63
77
|
end
|
|
64
78
|
end
|
|
@@ -486,12 +486,111 @@ module Mongoid
|
|
|
486
486
|
# @return [ Integer ] The number of documents deleted.
|
|
487
487
|
def remove_all(conditions = nil, method = :delete_all)
|
|
488
488
|
selector = conditions || {}
|
|
489
|
-
|
|
489
|
+
selector.merge!(criteria.selector)
|
|
490
|
+
|
|
491
|
+
# Scanning before the delete means scanning only what is already in
|
|
492
|
+
# memory, so it is only done where there is a pattern whose cost has
|
|
493
|
+
# to be bounded. Everywhere else the delete comes first and the scan
|
|
494
|
+
# sees the survivors, which is what this association has always
|
|
495
|
+
# done.
|
|
496
|
+
#
|
|
497
|
+
# The limit is read once and carried into whichever branch is taken.
|
|
498
|
+
# Letting the branch below ask again would read it a second time,
|
|
499
|
+
# across a server round trip, and a limit that had become positive
|
|
500
|
+
# in the meantime would put a deadline on the delete_if -- a query,
|
|
501
|
+
# and an unbind of every match -- which is the arrangement this
|
|
502
|
+
# branch exists to avoid.
|
|
503
|
+
limit = Matcher::RegexpBudget.limit_for(selector)
|
|
504
|
+
return remove_all_bounded(selector, method, limit) if limit
|
|
505
|
+
|
|
506
|
+
removed = klass.send(method, selector)
|
|
507
|
+
|
|
508
|
+
# No scope around this. The scan runs after the delete, so the
|
|
509
|
+
# association this loads comes back holding only what the delete did
|
|
510
|
+
# not match, and there is no pattern here to bound in any case.
|
|
511
|
+
# Opening a scope would save _matches? from deciding once per
|
|
512
|
+
# document -- 0.48us against 1.74us for the match itself -- but a
|
|
513
|
+
# scope with nothing to bound suppresses that decision for
|
|
514
|
+
# everything nested inside it, and loading the association runs
|
|
515
|
+
# find callbacks. A pattern in a selector one of those evaluates
|
|
516
|
+
# would go unguarded, which costs more than the saving is worth on a
|
|
517
|
+
# path that has just made two round trips.
|
|
490
518
|
_target.delete_if do |doc|
|
|
491
519
|
doc._matches?(selector).tap do |b|
|
|
492
520
|
unbind_one(doc) if b
|
|
493
521
|
end
|
|
494
522
|
end
|
|
523
|
+
|
|
524
|
+
removed
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Deletes all related documents matching a selector that carries a
|
|
528
|
+
# regular expression, with the whole scan under one regexp budget.
|
|
529
|
+
#
|
|
530
|
+
# The scan runs before anything is deleted, so a budget that runs out
|
|
531
|
+
# leaves both the database and the association untouched rather than
|
|
532
|
+
# reporting a failure for a delete that has already happened. It also
|
|
533
|
+
# means the budget is closed by the time the association is mutated,
|
|
534
|
+
# so where the budget is enforced with Timeout there is no window for
|
|
535
|
+
# the exception to land in the middle of an unbind.
|
|
536
|
+
#
|
|
537
|
+
# @param [ Hash ] selector The selector to delete with.
|
|
538
|
+
# @param [ Symbol ] method The deletion method to call.
|
|
539
|
+
# @param [ Float ] limit The seconds the scan may spend, as read by
|
|
540
|
+
# the caller when it chose this path.
|
|
541
|
+
#
|
|
542
|
+
# @return [ Integer ] The number of documents deleted.
|
|
543
|
+
def remove_all_bounded(selector, method, limit)
|
|
544
|
+
# Only what the association already holds. Iterating it instead
|
|
545
|
+
# would load the whole association before the delete, and for a
|
|
546
|
+
# broad pattern that is every document the association has: /.*/ is
|
|
547
|
+
# both the cheapest pattern an attacker can supply and the one that
|
|
548
|
+
# matches everything, so bounding the cost of the matching would
|
|
549
|
+
# have been paid for with an unbounded amount of memory. Nothing is
|
|
550
|
+
# missed by not loading, because the server evaluates the same
|
|
551
|
+
# selector for the delete itself.
|
|
552
|
+
#
|
|
553
|
+
# It also means the scan runs no query, so no deadline of ours can
|
|
554
|
+
# land on one. Where the budget is enforced with Timeout, covering
|
|
555
|
+
# a query would report a slow network as a regexp timeout and could
|
|
556
|
+
# deliver the asynchronous exception inside the driver's socket
|
|
557
|
+
# read.
|
|
558
|
+
documents = _target.in_memory
|
|
559
|
+
|
|
560
|
+
matching = Matcher::RegexpBudget.open_with(limit) do
|
|
561
|
+
documents.select { |doc| doc._matches?(selector) }
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
removed = klass.send(method, selector)
|
|
565
|
+
|
|
566
|
+
# The ids are already known to be in memory, so the loaded and added
|
|
567
|
+
# documents can be dropped directly. Enumerable#delete would look
|
|
568
|
+
# each one up by id and stop at the first hash that has it, which
|
|
569
|
+
# removes the wrong instance when both hashes hold one for the same
|
|
570
|
+
# id. Enumerable#delete_if drops it from both, as this does, but
|
|
571
|
+
# only after load_all!, which is the load this scan exists to avoid.
|
|
572
|
+
#
|
|
573
|
+
# Which documents get unbound has always depended on which ones
|
|
574
|
+
# happened to be in memory, and scanning only what is in memory
|
|
575
|
+
# keeps that. It matters for has_and_belongs_to_many, whose
|
|
576
|
+
# unbind_one pulls the id out of the foreign key array on _base and
|
|
577
|
+
# marks it dirty; the scan used to run after the delete, against an
|
|
578
|
+
# association that a cold proxy had just reloaded as holding only
|
|
579
|
+
# the survivors, so nothing matched and nothing was unbound.
|
|
580
|
+
#
|
|
581
|
+
# TODO: unbinding should not depend on what was in memory. Every
|
|
582
|
+
# matched document leaves the association, so every one of them
|
|
583
|
+
# should be unbound, which would also stop _base's foreign key
|
|
584
|
+
# array from keeping ids that no longer resolve. That is a
|
|
585
|
+
# behaviour change for has_and_belongs_to_many and belongs in a
|
|
586
|
+
# ticket of its own, with a release note; it should not ride along
|
|
587
|
+
# on this one.
|
|
588
|
+
matching.each do |doc|
|
|
589
|
+
unbind_one(doc)
|
|
590
|
+
_target._loaded.delete(doc._id)
|
|
591
|
+
_target._added.delete(doc._id)
|
|
592
|
+
end
|
|
593
|
+
|
|
495
594
|
removed
|
|
496
595
|
end
|
|
497
596
|
|
data/lib/mongoid/config.rb
CHANGED
|
@@ -158,6 +158,23 @@ module Mongoid
|
|
|
158
158
|
# to `:global_thread_pool`.
|
|
159
159
|
option :global_executor_concurrency, default: nil
|
|
160
160
|
|
|
161
|
+
# When this flag is true, it will be possible to add a record to a
|
|
162
|
+
# "has_many" or "has_and_belongs_to_many" association by passing that
|
|
163
|
+
# record's id in the nested attributes for another parent record, even
|
|
164
|
+
# when the record does not already belong to that association. For a
|
|
165
|
+
# "has_many" association this moves the record to the new parent.
|
|
166
|
+
#
|
|
167
|
+
# When this flag is false, an id in nested attributes is only resolved
|
|
168
|
+
# within the association itself, and anything else raises an error.
|
|
169
|
+
#
|
|
170
|
+
# The default is `false`. Note that allowing reparenting via nested attributes
|
|
171
|
+
# is a potential security risk, since it could allow a malicious user to move
|
|
172
|
+
# records that they do not own to a parent record that they do own.
|
|
173
|
+
#
|
|
174
|
+
# This option will be removed in Mongoid 10, and the only behavior will be
|
|
175
|
+
# as if this option were set to false.
|
|
176
|
+
option :allow_reparenting_via_nested_attributes, default: false
|
|
177
|
+
|
|
161
178
|
# When this flag is false, a document will become read-only only once the
|
|
162
179
|
# #readonly! method is called, and an error will be raised on attempting
|
|
163
180
|
# to save or update such documents, instead of just on delete. When this
|
|
@@ -198,6 +215,51 @@ module Mongoid
|
|
|
198
215
|
# See https://jira.mongodb.org/browse/MONGOID-5658 for more details.
|
|
199
216
|
option :around_callbacks_for_embeds, default: true
|
|
200
217
|
|
|
218
|
+
# The maximum number of seconds that evaluating a single query in memory
|
|
219
|
+
# may spend executing regular expressions. Queries against an embedded
|
|
220
|
+
# association are evaluated in the calling thread, so a pattern built from
|
|
221
|
+
# user input runs locally and can otherwise consume unbounded CPU. The
|
|
222
|
+
# limit is cumulative over the whole query, since cost grows with the
|
|
223
|
+
# number of documents and conditions as well as with the pattern.
|
|
224
|
+
#
|
|
225
|
+
# Set to nil to remove the limit. On Ruby 3.2 and later the remaining
|
|
226
|
+
# budget is compiled into the pattern, so the limit counts only the time
|
|
227
|
+
# spent matching. Earlier Rubies have no per-Regexp timeout, so the query
|
|
228
|
+
# is bounded with Timeout instead and the limit is wall clock over the
|
|
229
|
+
# whole in-memory evaluation.
|
|
230
|
+
#
|
|
231
|
+
# See https://jira.mongodb.org/browse/MONGOID-5981 for details.
|
|
232
|
+
option :in_memory_regexp_time_limit, default: 5.0
|
|
233
|
+
|
|
234
|
+
# When false (default), query operators are restricted when building a
|
|
235
|
+
# selector, so that user-supplied input reaching the query builder cannot
|
|
236
|
+
# make MongoDB execute arbitrary JavaScript. Two rules are enforced:
|
|
237
|
+
#
|
|
238
|
+
# - An operator at the top level of an expression must appear in
|
|
239
|
+
# +Criteria::Queryable::Selectable::ALLOWED_QUERY_OPERATORS+.
|
|
240
|
+
# - +$where+, +$function+, and +$accumulator+ are rejected at any depth,
|
|
241
|
+
# including inside +$expr+ and the logical operators.
|
|
242
|
+
#
|
|
243
|
+
# This applies to every query method that accepts an expression, including
|
|
244
|
+
# +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
|
|
245
|
+
# It also governs the string form of +where+, e.g.
|
|
246
|
+
# +where("this.name == 'admin'")+, which the server evaluates as +$where+.
|
|
247
|
+
# Applications relying on that form must set this option to true.
|
|
248
|
+
#
|
|
249
|
+
# The APIs that request JavaScript explicitly, +Criteria#for_js+ and
|
|
250
|
+
# +js_query+, are unaffected: there the developer has asked for it.
|
|
251
|
+
#
|
|
252
|
+
# Set to true to restore the unrestricted pass-through behavior.
|
|
253
|
+
#
|
|
254
|
+
# Note that this option is deliberately not tied to +load_defaults+: an
|
|
255
|
+
# application that has opted into older defaults still gets the guard, and
|
|
256
|
+
# must set this option explicitly to turn it off.
|
|
257
|
+
#
|
|
258
|
+
# See https://jira.mongodb.org/browse/MONGOID-5939,
|
|
259
|
+
# https://jira.mongodb.org/browse/MONGOID-5993,
|
|
260
|
+
# https://jira.mongodb.org/browse/MONGOID-5994 for details.
|
|
261
|
+
option :allow_unsafe_query_operators, default: false
|
|
262
|
+
|
|
201
263
|
# Returns the Config singleton, for use in the configure DSL.
|
|
202
264
|
#
|
|
203
265
|
# @return [ self ] The Config singleton.
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'mongoid/field_readable'
|
|
4
|
+
|
|
3
5
|
module Mongoid
|
|
4
6
|
module Contextual
|
|
5
7
|
module Aggregable
|
|
6
8
|
# Contains behavior for aggregating values in memory.
|
|
7
9
|
module Memory
|
|
10
|
+
include FieldReadable
|
|
8
11
|
|
|
9
12
|
# Get all the aggregate values for the provided field.
|
|
10
13
|
# Provided for interface consistency with Aggregable::Mongo.
|
|
@@ -29,7 +32,7 @@ module Mongoid
|
|
|
29
32
|
#
|
|
30
33
|
# @return [ Numeric ] The average.
|
|
31
34
|
def avg(field)
|
|
32
|
-
total = count { |doc| !doc
|
|
35
|
+
total = count { |doc| !read_field_value(doc, field).nil? }
|
|
33
36
|
return nil unless total > 0
|
|
34
37
|
|
|
35
38
|
total = total.to_f if total.is_a?(Integer)
|
|
@@ -114,7 +117,7 @@ module Mongoid
|
|
|
114
117
|
def aggregate_by(field, method)
|
|
115
118
|
return nil unless any?
|
|
116
119
|
|
|
117
|
-
map { |doc| doc
|
|
120
|
+
map { |doc| read_field_value(doc, field) }.compact.public_send(method)
|
|
118
121
|
end
|
|
119
122
|
end
|
|
120
123
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "mongoid/contextual/aggregable/memory"
|
|
4
4
|
require "mongoid/association/eager_loadable"
|
|
5
|
+
require "mongoid/field_readable"
|
|
5
6
|
|
|
6
7
|
module Mongoid
|
|
7
8
|
module Contextual
|
|
@@ -11,6 +12,7 @@ module Mongoid
|
|
|
11
12
|
include Association::EagerLoadable
|
|
12
13
|
include Queryable
|
|
13
14
|
include Positional
|
|
15
|
+
include FieldReadable
|
|
14
16
|
|
|
15
17
|
# @attribute [r] root The root document.
|
|
16
18
|
# @attribute [r] path The atomic path.
|
|
@@ -170,10 +172,20 @@ module Mongoid
|
|
|
170
172
|
# @param [ Criteria ] criteria The criteria.
|
|
171
173
|
def initialize(criteria)
|
|
172
174
|
@criteria, @klass = criteria, criteria.klass
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
|
|
176
|
+
# Resolving the collection can build a Mongo::Client the first time it
|
|
177
|
+
# runs, so it happens before the budget opens. Where the budget is
|
|
178
|
+
# enforced with Timeout the exception is asynchronous, and landing in
|
|
179
|
+
# the middle of that would leave a half-built client behind.
|
|
180
|
+
if (first = criteria.documents.first)
|
|
181
|
+
@root = first._root
|
|
182
|
+
@collection = @root.collection
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# One regexp budget covers the whole scan, so that the limit bounds the
|
|
186
|
+
# query rather than each document individually.
|
|
187
|
+
@documents = Matcher::RegexpBudget.open(criteria.selector) do
|
|
188
|
+
criteria.documents.select { |doc| doc._matches?(criteria.selector) }
|
|
177
189
|
end
|
|
178
190
|
apply_sorting
|
|
179
191
|
apply_options
|
|
@@ -730,11 +742,10 @@ module Mongoid
|
|
|
730
742
|
# _translations hash so that we can get the specified translation in
|
|
731
743
|
# the remaining
|
|
732
744
|
if field&.localized?
|
|
733
|
-
document.
|
|
745
|
+
document.public_send("#{segment}_translations")
|
|
734
746
|
end
|
|
735
747
|
end
|
|
736
|
-
|
|
737
|
-
res.nil? ? document.try(meth) : res
|
|
748
|
+
res.nil? ? read_field_value(document, segment) : res
|
|
738
749
|
elsif document.is_a?(Hash)
|
|
739
750
|
# TODO: Remove the indifferent access when implementing MONGOID-5410.
|
|
740
751
|
document.key?(segment.to_s) ?
|
|
@@ -325,6 +325,10 @@ module Mongoid
|
|
|
325
325
|
end
|
|
326
326
|
end
|
|
327
327
|
end
|
|
328
|
+
# Every query method that takes a user-supplied expression normalizes
|
|
329
|
+
# it here, so this is where the operator guard is enforced. See
|
|
330
|
+
# Selectable#_mongoid_validate_operators! for what it does not cover.
|
|
331
|
+
_mongoid_validate_operators!(result)
|
|
328
332
|
result
|
|
329
333
|
end
|
|
330
334
|
|
|
@@ -343,6 +347,14 @@ module Mongoid
|
|
|
343
347
|
if criterion.is_a?(Selectable)
|
|
344
348
|
criterion = criterion.selector
|
|
345
349
|
end
|
|
350
|
+
|
|
351
|
+
# The __override__ path writes to the selector directly, so it
|
|
352
|
+
# bypasses _mongoid_expand_keys where the operator guard normally
|
|
353
|
+
# runs. Enforce the guard here so chained override methods
|
|
354
|
+
# (elem_match, exists, eq, gt, in, etc.) cannot smuggle $where or
|
|
355
|
+
# other JavaScript operators under strict mode.
|
|
356
|
+
_mongoid_validate_operators!(criterion)
|
|
357
|
+
|
|
346
358
|
selection(criterion) do |selector, field, value|
|
|
347
359
|
expression = prepare(field, operator, value)
|
|
348
360
|
existing = selector[field]
|
|
@@ -822,6 +822,12 @@ module Mongoid
|
|
|
822
822
|
# only ever specify one criterion to #where.
|
|
823
823
|
@criterion = criterion
|
|
824
824
|
if criterion.is_a?(String)
|
|
825
|
+
unless Mongoid.allow_unsafe_query_operators?
|
|
826
|
+
raise Errors::InvalidQuery,
|
|
827
|
+
"String criteria are not allowed because they compile to the '$where' operator, " \
|
|
828
|
+
'which is not allowed in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
829
|
+
'to permit all operators.'
|
|
830
|
+
end
|
|
825
831
|
js_query(criterion)
|
|
826
832
|
else
|
|
827
833
|
expr_query(criterion)
|
|
@@ -831,6 +837,19 @@ module Mongoid
|
|
|
831
837
|
|
|
832
838
|
private
|
|
833
839
|
|
|
840
|
+
# Operators permitted at the top level of a query expression without
|
|
841
|
+
# opt-in. Excludes $where (JS execution) and other operators not needed
|
|
842
|
+
# for ordinary application queries.
|
|
843
|
+
ALLOWED_QUERY_OPERATORS = %w[
|
|
844
|
+
$and $or $nor $not $text $comment $expr $jsonSchema $alwaysFalse $alwaysTrue
|
|
845
|
+
].freeze
|
|
846
|
+
|
|
847
|
+
# Operators that execute server-side JavaScript. These are rejected at
|
|
848
|
+
# any depth, not just at the top level: the allowlist above permits
|
|
849
|
+
# $expr and the logical operators, and their values are arbitrary
|
|
850
|
+
# nested expressions that can carry $function or $where.
|
|
851
|
+
JAVASCRIPT_QUERY_OPERATORS = %w[$where $function $accumulator].freeze
|
|
852
|
+
|
|
834
853
|
# Adds the specified expression to the query.
|
|
835
854
|
#
|
|
836
855
|
# Criterion must be a hash in one of the following forms:
|
|
@@ -856,6 +875,8 @@ module Mongoid
|
|
|
856
875
|
raise Errors::InvalidQuery, "Expression must be a Hash: #{Errors::InvalidQuery.truncate_expr(criterion)}"
|
|
857
876
|
end
|
|
858
877
|
|
|
878
|
+
# The operator guard is applied by _mongoid_expand_keys, which every
|
|
879
|
+
# query method that accepts a user-supplied expression passes through.
|
|
859
880
|
normalized = _mongoid_expand_keys(criterion)
|
|
860
881
|
clone.tap do |query|
|
|
861
882
|
normalized.each do |field, value|
|
|
@@ -871,6 +892,94 @@ module Mongoid
|
|
|
871
892
|
end
|
|
872
893
|
end
|
|
873
894
|
|
|
895
|
+
# Enforces the operator rules governed by the
|
|
896
|
+
# +allow_unsafe_query_operators+ configuration option against a
|
|
897
|
+
# normalized query expression.
|
|
898
|
+
#
|
|
899
|
+
# Two rules apply, and both are skipped when the option is true:
|
|
900
|
+
#
|
|
901
|
+
# - An operator at the top level of the expression must appear in
|
|
902
|
+
# ALLOWED_QUERY_OPERATORS.
|
|
903
|
+
# - An operator in JAVASCRIPT_QUERY_OPERATORS is rejected at any depth.
|
|
904
|
+
#
|
|
905
|
+
# This is called from #_mongoid_expand_keys rather than from the
|
|
906
|
+
# individual query methods, because that is the one point every query
|
|
907
|
+
# method taking a user-supplied expression passes through on its way to
|
|
908
|
+
# the selector.
|
|
909
|
+
#
|
|
910
|
+
# It deliberately does not cover the APIs that ask for JavaScript
|
|
911
|
+
# outright, such as #js_query and Criteria#for_js: there the developer
|
|
912
|
+
# has chosen server-side JavaScript, so there is nothing to guard
|
|
913
|
+
# against. The same goes for the low-level Storable methods
|
|
914
|
+
# (#add_field_expression, #add_operator_expression), which write to the
|
|
915
|
+
# selector directly.
|
|
916
|
+
#
|
|
917
|
+
# @param [ Hash ] expr A normalized query expression.
|
|
918
|
+
#
|
|
919
|
+
# @raise [ Errors::InvalidQuery ] If a disallowed operator is present.
|
|
920
|
+
#
|
|
921
|
+
# @api private
|
|
922
|
+
def _mongoid_validate_operators!(expr)
|
|
923
|
+
return if Mongoid.allow_unsafe_query_operators?
|
|
924
|
+
|
|
925
|
+
expr.each_key do |field|
|
|
926
|
+
field_s = field.to_s
|
|
927
|
+
next unless field_s.start_with?('$')
|
|
928
|
+
next if ALLOWED_QUERY_OPERATORS.include?(field_s)
|
|
929
|
+
|
|
930
|
+
raise Errors::InvalidQuery,
|
|
931
|
+
"Operator '#{field_s}' is not allowed in a query expression. " \
|
|
932
|
+
'Set Mongoid.allow_unsafe_query_operators = true to permit all operators.'
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
_mongoid_validate_no_javascript!(expr)
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
# Walks a query expression looking for operators that execute
|
|
939
|
+
# server-side JavaScript, descending through both hashes and arrays so
|
|
940
|
+
# that nested forms such as {'$expr' => {'$function' => ...}} and
|
|
941
|
+
# {'$or' => [ {'$where' => ...} ]} are caught.
|
|
942
|
+
#
|
|
943
|
+
# The walk does not distinguish operator position from value position,
|
|
944
|
+
# so it also rejects queries where a JavaScript operator name appears as
|
|
945
|
+
# data rather than as an operator. Server 5.0+ permits $-prefixed field
|
|
946
|
+
# names in stored documents, which makes this reachable:
|
|
947
|
+
#
|
|
948
|
+
# Doc.where(payload: { '$eq' => { '$function' => 'abc' } })
|
|
949
|
+
#
|
|
950
|
+
# Here the $eq marks its argument as a literal value to compare, so the
|
|
951
|
+
# server never evaluates it, but the guard raises anyway. The only
|
|
952
|
+
# workaround today is the global allow_unsafe_query_operators flag.
|
|
953
|
+
#
|
|
954
|
+
# TODO: discuss whether to track operator position (skipping the subtree
|
|
955
|
+
# under $eq, $ne, $in, $nin, and $elemMatch values) in a future
|
|
956
|
+
# iteration. It removes the false positive but adds exactly the kind of
|
|
957
|
+
# state that a real bypass could hide in, so it was left out for now.
|
|
958
|
+
#
|
|
959
|
+
# @param [ Object ] object A fragment of a query expression.
|
|
960
|
+
#
|
|
961
|
+
# @raise [ Errors::InvalidQuery ] If a JavaScript operator is present.
|
|
962
|
+
#
|
|
963
|
+
# @api private
|
|
964
|
+
def _mongoid_validate_no_javascript!(object)
|
|
965
|
+
case object
|
|
966
|
+
when Hash
|
|
967
|
+
object.each do |key, value|
|
|
968
|
+
key_s = key.to_s
|
|
969
|
+
if JAVASCRIPT_QUERY_OPERATORS.include?(key_s)
|
|
970
|
+
raise Errors::InvalidQuery,
|
|
971
|
+
"Operator '#{key_s}' executes server-side JavaScript and is not allowed " \
|
|
972
|
+
'anywhere in a query expression. Set Mongoid.allow_unsafe_query_operators = true ' \
|
|
973
|
+
'to permit all operators.'
|
|
974
|
+
end
|
|
975
|
+
|
|
976
|
+
_mongoid_validate_no_javascript!(value)
|
|
977
|
+
end
|
|
978
|
+
when Array
|
|
979
|
+
object.each { |value| _mongoid_validate_no_javascript!(value) }
|
|
980
|
+
end
|
|
981
|
+
end
|
|
982
|
+
|
|
874
983
|
# Force the values of the criterion to be evolved.
|
|
875
984
|
#
|
|
876
985
|
# @api private
|
|
@@ -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
|
data/lib/mongoid/errors.rb
CHANGED
|
@@ -10,6 +10,7 @@ require "mongoid/errors/document_not_found"
|
|
|
10
10
|
require "mongoid/errors/empty_config_file"
|
|
11
11
|
require "mongoid/errors/immutable_attribute"
|
|
12
12
|
require "mongoid/errors/in_memory_collation_not_supported"
|
|
13
|
+
require "mongoid/errors/in_memory_regexp_timeout"
|
|
13
14
|
require "mongoid/errors/invalid_async_query_executor"
|
|
14
15
|
require "mongoid/errors/invalid_collection"
|
|
15
16
|
require "mongoid/errors/invalid_config_file"
|