FIX: ensure no infinite category loop

If there's ever a circular reference in categories, don't go into an infinite loop when generating the category slug.

Instead, keep track of parent ids, and bail out as soon as we're encountering one more than once.
This commit is contained in:
Régis Hanol 2024-05-06 17:01:05 +02:00
parent f72f63660a
commit 10f77556cd
2 changed files with 37 additions and 8 deletions

View File

@ -1091,11 +1091,13 @@ class Category < ActiveRecord::Base
.find_each { |category| category.create_category_definition } .find_each { |category| category.create_category_definition }
end end
def slug_path def slug_path(parent_ids = Set.new)
if self.parent_category_id.present? if self.parent_category_id.present?
slug_path = self.parent_category.slug_path if parent_ids.add?(self.parent_category_id)
slug_path.push(self.slug_for_url) self.parent_category.slug_path(parent_ids) << self.slug_for_url
slug_path else
[]
end
else else
[self.slug_for_url] [self.slug_for_url]
end end

View File

@ -47,8 +47,7 @@ RSpec.describe Category do
it "should delete associated sidebar_section_links when category is destroyed" do it "should delete associated sidebar_section_links when category is destroyed" do
category_sidebar_section_link = Fabricate(:category_sidebar_section_link) category_sidebar_section_link = Fabricate(:category_sidebar_section_link)
category_sidebar_section_link_2 = Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
Fabricate(:category_sidebar_section_link, linkable: category_sidebar_section_link.linkable)
tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link) tag_sidebar_section_link = Fabricate(:tag_sidebar_section_link)
expect { category_sidebar_section_link.linkable.destroy! }.to change { expect { category_sidebar_section_link.linkable.destroy! }.to change {
@ -996,7 +995,7 @@ RSpec.describe Category do
) )
category.clear_auto_bump_cache! category.clear_auto_bump_cache!
post1 = create_post(category: category, created_at: 15.seconds.ago) create_post(category: category, created_at: 15.seconds.ago)
# no limits on post creation or category creation please # no limits on post creation or category creation please
RateLimiter.enable RateLimiter.enable
@ -1419,7 +1418,7 @@ RSpec.describe Category do
it 'returns nil when input is ["category:invalid-slug:sub-subcategory"] and maximum category nesting is 3' do it 'returns nil when input is ["category:invalid-slug:sub-subcategory"] and maximum category nesting is 3' do
SiteSetting.max_category_nesting = 3 SiteSetting.max_category_nesting = 3
sub_subcategory = Fabricate(:category, parent_category: subcategory, slug: "sub-subcategory") Fabricate(:category, parent_category: subcategory, slug: "sub-subcategory")
expect(Category.ids_from_slugs(%w[category:invalid-slug:sub-subcategory])).to eq([]) expect(Category.ids_from_slugs(%w[category:invalid-slug:sub-subcategory])).to eq([])
end end
@ -1464,6 +1463,34 @@ RSpec.describe Category do
end end
end end
describe "#slug_path" do
before { SiteSetting.max_category_nesting = 3 }
fab!(:grandparent) { Fabricate(:category, slug: "foo") }
fab!(:parent) { Fabricate(:category, parent_category: grandparent, slug: "bar") }
let(:child) { Fabricate(:category, parent_category: parent, slug: "boo") }
it "returns the slug for categories without parents" do
expect(grandparent.slug_path).to eq [grandparent.slug]
end
it "returns the slug for categories with parent" do
expect(parent.slug_path).to eq [grandparent.slug, parent.slug]
end
it "returns the slug for categories with grand-parent" do
expect(child.slug_path).to eq [grandparent.slug, parent.slug, child.slug]
end
it "avoids infinite loops with circular references" do
grandparent.parent_category = parent
grandparent.save!(validate: false)
expect(grandparent.slug_path).to eq [parent.slug, grandparent.slug]
expect(parent.slug_path).to eq [grandparent.slug, parent.slug]
end
end
describe "#slug_ref" do describe "#slug_ref" do
fab!(:category) { Fabricate(:category, slug: "foo") } fab!(:category) { Fabricate(:category, slug: "foo") }