diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 23:54:38 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-10 23:54:38 +0200 |
| commit | ab392d472c15eee3618cb7c02b2b4707151598b6 (patch) | |
| tree | 70743f8fa0df9b75b0debb8f2724ea947ad99175 | |
| parent | 7d6a665b896252537b8b8df2f15e204d04417231 (diff) | |
Prevent a staged parent move from crashing on its own descendant
publish_draft! called move_to_child_of before validating the new
parent at all. Staging a node under one of its own descendants
created a real, if transient, cycle in parent_id the instant that
save landed -- and update_unique_names_of_children's after_save
callback, which recurses down through parent_id with no cycle check
and no depth limit, bounced between the two nodes forever, crashing
the whole process with a SystemStackError rather than just producing
bad data.
Now rejected up front with a normal ActiveRecord::RecordInvalid.
| -rw-r--r-- | app/models/node.rb | 5 | ||||
| -rw-r--r-- | test/models/node_test.rb | 12 |
2 files changed, 17 insertions, 0 deletions
diff --git a/app/models/node.rb b/app/models/node.rb index 82d9954..9cb4840 100644 --- a/app/models/node.rb +++ b/app/models/node.rb | |||
| @@ -227,6 +227,11 @@ class Node < ApplicationRecord | |||
| 227 | 227 | ||
| 228 | if staged_parent_id && (staged_parent_id != parent_id) | 228 | if staged_parent_id && (staged_parent_id != parent_id) |
| 229 | new_parent = Node.find(staged_parent_id) | 229 | new_parent = Node.find(staged_parent_id) |
| 230 | |||
| 231 | if new_parent == self || self.descendants.include?(new_parent) | ||
| 232 | raise ActiveRecord::RecordInvalid.new(self), "Cannot move a node under itself or one of its own descendants" | ||
| 233 | end | ||
| 234 | |||
| 230 | self.staged_parent_id = nil | 235 | self.staged_parent_id = nil |
| 231 | self.save! | 236 | self.save! |
| 232 | self.move_to_child_of(new_parent) | 237 | self.move_to_child_of(new_parent) |
diff --git a/test/models/node_test.rb b/test/models/node_test.rb index 280614e..5626384 100644 --- a/test/models/node_test.rb +++ b/test/models/node_test.rb | |||
| @@ -499,4 +499,16 @@ class NodeTest < ActiveSupport::TestCase | |||
| 499 | node.autosave!({title: "v3"}, user) | 499 | node.autosave!({title: "v3"}, user) |
| 500 | assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D | 500 | assert_equal [[:head, :draft], [:draft, :autosave]], node.available_layer_pairs # state D |
| 501 | end | 501 | end |
| 502 | |||
| 503 | test "publishing a staged move under one's own descendant is rejected, not allowed to crash" do | ||
| 504 | a = Node.root.children.create!(:slug => "cycle_guard_a") | ||
| 505 | b = a.children.create!(:slug => "cycle_guard_b") | ||
| 506 | |||
| 507 | a.staged_parent_id = b.id | ||
| 508 | |||
| 509 | assert_raises(ActiveRecord::RecordInvalid) { a.publish_draft! } | ||
| 510 | |||
| 511 | a.reload | ||
| 512 | assert_equal Node.root.id, a.parent_id | ||
| 513 | end | ||
| 502 | end | 514 | end |
