0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-05 23:04:22 +08:00
discourse/app/services/sidebar_section_updater.rb
Isaac Janzen d041a4538c
FIX: Enforce per-section sidebar link cap on partial sidebar section updates (#42277)
## Summary

Prevent authenticated users from exceeding the configured per-section
sidebar link limit by repeatedly appending link batches through partial
update requests. The patch validates the final, non-destroyed link count
at the model boundary and serializes concurrent updates with a row lock
so the cap holds.

## Source

- Patch Triage: https://patch.discourse.org/patch-triage/1583

Co-authored-by: discourse-patch-triage
<272280883+discourse-patch-triage[bot]@users.noreply.github.com>
2026-08-03 14:52:12 -05:00

63 lines
1.8 KiB
Ruby
Vendored

# frozen_string_literal: true
class SidebarSectionUpdater
def self.update!(sidebar_section:, user:, section_params:, links_params:)
new(sidebar_section:, user:, section_params:, links_params:).update!
end
def initialize(sidebar_section:, user:, section_params:, links_params:)
@sidebar_section = sidebar_section
@user = user
@section_params = section_params
@links_params = (links_params || []).map { |link| link.to_h.with_indifferent_access }
end
def update!
@sidebar_section.with_lock do
@sidebar_section.update!(@section_params.merge(sidebar_urls_attributes: @links_params))
@sidebar_section.sidebar_section_links.update_all(user_id: @sidebar_section.user_id)
update_link_order
end
publish_public_update if @sidebar_section.public?
@sidebar_section
end
private
def update_link_order
order =
@sidebar_section
.sidebar_urls
.sort_by do |url|
@links_params.index { |link| link[:name] == url.name && link[:value] == url.value } || -1
end
.each_with_index
.map { |url, index| [url.id, index] }
.to_h
set_order(order)
end
def set_order(order)
position_generator =
(0..@sidebar_section.sidebar_section_links.count * 2).excluding(
@sidebar_section.sidebar_section_links.map(&:position),
).each
links =
@sidebar_section
.sidebar_section_links
.sort_by { |link| order[link.linkable_id] }
.map { |link| link.attributes.merge(position: position_generator.next) }
@sidebar_section.sidebar_section_links.upsert_all(links, update_only: [:position])
end
def publish_public_update
StaffActionLogger.new(@user).log_update_public_sidebar_section(@sidebar_section)
MessageBus.publish("/refresh-sidebar-sections", nil)
Site.clear_anon_cache!
end
end