mirror of
https://github.com/discourse/discourse.git
synced 2026-08-06 13:08:40 +08:00
Currently, theme settings with `type: list` and `list_type: group` require client-side permission checks, but `currentUser.groups` only includes visible groups, not all groups the user belongs to. This makes permission checks unreliable and can leak hidden group membership. To address this, this commit adds an opt-in `resolve_group_membership: true` option that replaces the group ID list with a user_in_SETTING_NAME boolean resolved server-side via `guardian.in_any_groups?`. The original group list is removed from the frontend payload to prevent leaking group IDs. Since theme settings are cached per-theme (not per-user), the resolution happens after the cache lookup during per-request serialization in `ApplicationLayoutPreloader#activated_themes_json`. Example YAML: ```yaml copy_button_allowed_groups: type: list list_type: group resolve_group_membership: true default: "1|3" ``` Frontend usage: ```javascript if (!settings.user_in_copy_button_allowed_groups) return; ```
79 lines
2.3 KiB
Ruby
Vendored
79 lines
2.3 KiB
Ruby
Vendored
# frozen_string_literal: true
|
|
|
|
class ThemeSettingsParser
|
|
class InvalidYaml < StandardError
|
|
end
|
|
|
|
def initialize(setting_field)
|
|
@setting_field = setting_field
|
|
@types = ThemeSetting.types
|
|
end
|
|
|
|
def extract_description(desc)
|
|
return desc if desc.is_a?(String)
|
|
|
|
if desc.is_a?(Hash)
|
|
default_locale = SiteSetting.default_locale.to_sym
|
|
fallback_locale = desc.keys.find { |key| I18n.locale_available?(key) }
|
|
locale = desc[I18n.locale] || desc[default_locale] || desc[:en] || desc[fallback_locale]
|
|
|
|
locale if locale.is_a?(String)
|
|
end
|
|
end
|
|
|
|
def create_opts(default, type, raw_opts = {})
|
|
opts = {}
|
|
opts[:description] = extract_description(raw_opts[:description])
|
|
|
|
if type == @types[:enum]
|
|
choices = raw_opts[:choices]
|
|
choices = [] unless choices.is_a?(Array)
|
|
choices << default if choices.exclude?(default)
|
|
opts[:choices] = choices
|
|
end
|
|
|
|
if [@types[:integer], @types[:string], @types[:float]].include?(type)
|
|
opts[:max] = raw_opts[:max].is_a?(Numeric) ? raw_opts[:max] : Float::INFINITY
|
|
opts[:min] = raw_opts[:min].is_a?(Numeric) ? raw_opts[:min] : -Float::INFINITY
|
|
end
|
|
|
|
opts[:list_type] = raw_opts[:list_type] if raw_opts[:list_type]
|
|
opts[:resolve_group_membership] = !!raw_opts[:resolve_group_membership]
|
|
|
|
opts[:textarea] = !!raw_opts[:textarea]
|
|
opts[:json_schema] = raw_opts[:json_schema]
|
|
opts[:schema] = raw_opts[:schema]
|
|
|
|
opts[:refresh] = !!raw_opts[:refresh]
|
|
|
|
opts
|
|
end
|
|
|
|
def load
|
|
return if @setting_field.value.blank?
|
|
|
|
begin
|
|
parsed = YAML.safe_load(@setting_field.value)
|
|
rescue Psych::SyntaxError, Psych::DisallowedClass => e
|
|
raise InvalidYaml.new(e.message)
|
|
end
|
|
raise InvalidYaml.new(I18n.t("themes.settings_errors.invalid_yaml")) unless parsed.is_a?(Hash)
|
|
|
|
parsed.deep_symbolize_keys!
|
|
|
|
parsed.each_pair do |setting, value|
|
|
if (type = ThemeSetting.guess_type(value)).present?
|
|
result = [setting, value, type, create_opts(value, type)]
|
|
elsif (hash = value).is_a?(Hash)
|
|
default = hash[:default]
|
|
type = hash.key?(:type) ? @types[hash[:type]&.to_sym] : ThemeSetting.guess_type(default)
|
|
|
|
result = [setting, default, type, create_opts(default, type, hash)]
|
|
else
|
|
result = [setting, value, nil, {}]
|
|
end
|
|
|
|
yield(*result)
|
|
end
|
|
end
|
|
end
|