mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-04-30 07:07:10 +08:00
When a site setting has `depends_behavior: hidden`, it is shown or hidden based on whether its parent setting is enabled. Until now that decision was made on the server, so toggling the parent in the admin UI required saving and reloading the page for dependent settings to appear or disappear. The server no longer excludes hidden-by-dependency settings from the `/admin/site_settings` payload; it just serializes `depends_behavior` alongside `depends_on` and lets the client decide. A new `site-setting-store` service tracks every loaded setting by name and exposes `isSettingVisible(setting, activeFilter)`, the single source of truth for the following rule: - On first load, children of a disabled parent are not rendered. - Enabling the parent (checkbox or reset-to-default) reveals them reactively; a `revealed` latch on the model keeps them visible for the rest of the session. - Disabling the parent again after reveal leaves the children mounted and switches them to a disabled state (no re-hide jank). - Searching by exact setting name still surfaces an unrevealed child (in a disabled state), so deep links like `?filter=topic_voting_tl0_vote_limit` work even when the parent is off. Visibility is enforced at the iteration layer (category controller and `admin-filtered-site-settings`) using the shared helper, so the filter recomputes whenever `revealed` flips on a tracked `SiteSetting`. Ref - t/181881 --------- Co-authored-by: Martin Brennan <martin@discourse.org>
1393 lines
44 KiB
Ruby
1393 lines
44 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module SiteSettingExtension
|
|
include SiteSettings::DeprecatedSettings
|
|
include HasSanitizableFields
|
|
|
|
SiteSettingChangeResult = Struct.new(:previous_value, :new_value)
|
|
InvalidSettingAccess = Class.new(StandardError)
|
|
|
|
delegate :description, :keywords, :placeholder, :humanized_name, to: SiteSettings::LabelFormatter
|
|
|
|
# support default_locale being set via global settings
|
|
# this also adds support for testing the extension and global settings
|
|
# for site locale
|
|
def self.extended(klass)
|
|
if GlobalSetting.respond_to?(:default_locale) && GlobalSetting.default_locale.present?
|
|
# protected
|
|
klass.send :setup_shadowed_methods, :default_locale, GlobalSetting.default_locale
|
|
end
|
|
end
|
|
|
|
# we need a default here to support defaults per locale
|
|
def default_locale=(val)
|
|
val = val.to_s
|
|
raise Discourse::InvalidParameters.new(:value) unless LocaleSiteSetting.valid_value?(val)
|
|
if val != self.default_locale
|
|
add_override!(:default_locale, val)
|
|
refresh!
|
|
Discourse.request_refresh!
|
|
end
|
|
end
|
|
|
|
def default_locale?
|
|
true
|
|
end
|
|
|
|
# set up some sort of default so we can look stuff up
|
|
def default_locale
|
|
# note optimised cause this is called a lot so avoiding .presence which
|
|
# adds 2 method calls
|
|
locale = current[:default_locale]
|
|
if locale && locale.present?
|
|
locale
|
|
else
|
|
SiteSettings::DefaultsProvider::DEFAULT_LOCALE
|
|
end
|
|
end
|
|
|
|
def has_setting?(v)
|
|
defaults.has_setting?(v)
|
|
end
|
|
|
|
def supported_types
|
|
SiteSettings::TypeSupervisor.supported_types
|
|
end
|
|
|
|
def types
|
|
SiteSettings::TypeSupervisor.types
|
|
end
|
|
|
|
def listen_for_changes=(val)
|
|
@listen_for_changes = val
|
|
end
|
|
|
|
def provider=(val)
|
|
@provider = val
|
|
refresh!
|
|
end
|
|
|
|
def provider
|
|
@provider ||= SiteSettings::DbProvider.new(SiteSetting)
|
|
end
|
|
|
|
def mutex
|
|
@mutex ||= Mutex.new
|
|
end
|
|
|
|
# Represents the current values of all site settings, incorporating
|
|
# the database values and merging them with shadowed settings and
|
|
# default values.
|
|
def current
|
|
@containers ||= {}
|
|
@containers[provider.current_site] ||= {}
|
|
end
|
|
|
|
# Represents settings that actually have a value saved in the
|
|
# database. We currently even store the default value in the
|
|
# DB if an admin saves a different value then changes back
|
|
# to the default.
|
|
def modified
|
|
@modified ||= {}
|
|
@modified[provider.current_site] ||= {}
|
|
end
|
|
|
|
# Represents a map of theme IDs to all theme site settings
|
|
# and values.
|
|
def theme_site_settings
|
|
@theme_site_settings ||= {}
|
|
@theme_site_settings[provider.current_site] ||= {}
|
|
end
|
|
|
|
#
|
|
def humanized_names(name)
|
|
@humanized_names ||= {}
|
|
@humanized_names[name] ||= humanized_name(name)
|
|
end
|
|
|
|
# Used for upcoming changes settings to determine which specific
|
|
# groups have the change turned on for them. This is done separately
|
|
# from group-based site settings because upcoming change settings
|
|
# are always booleans.
|
|
def site_setting_group_ids
|
|
@site_setting_group_ids ||= {}
|
|
@site_setting_group_ids[provider.current_site] ||= {}
|
|
end
|
|
|
|
def defaults
|
|
@defaults ||= SiteSettings::DefaultsProvider.new(self)
|
|
end
|
|
|
|
def type_supervisor
|
|
@type_supervisor ||= SiteSettings::TypeSupervisor.new(defaults)
|
|
end
|
|
|
|
def categories
|
|
@categories ||= {}
|
|
end
|
|
|
|
def themeable
|
|
@themeable ||= {}
|
|
end
|
|
|
|
def areas
|
|
@areas ||= {}
|
|
end
|
|
|
|
def mandatory_values
|
|
@mandatory_values ||= {}
|
|
end
|
|
|
|
def disallowed_groups
|
|
@disallowed_groups ||= {}
|
|
end
|
|
|
|
def shadowed_settings
|
|
@shadowed_settings ||= Set.new
|
|
end
|
|
|
|
def requires_confirmation_settings
|
|
@requires_confirmation_settings ||= {}
|
|
end
|
|
|
|
# Valid upcoming change metadata looks like this
|
|
# in site_settings.yml:
|
|
#
|
|
# setting_name:
|
|
# default: false
|
|
# client: true
|
|
# hidden: true
|
|
# upcoming_change:
|
|
# status: "alpha" (see UpcomingChanges.statuses.keys)
|
|
# impact: "feature,staff" (feature|other for the first part, staff|admins|moderators|all_members|developers for the second part)
|
|
# learn_more_url: ""
|
|
def upcoming_change_metadata
|
|
@upcoming_change_metadata ||= {}
|
|
end
|
|
|
|
# Has a pointer from a site setting name to the upcoming change name
|
|
# and overriden default value. Looks like this in site_settings.yml:
|
|
#
|
|
# setting_name:
|
|
# upcoming_change_default_override:
|
|
# upcoming_change: upcoming_change_name
|
|
# new_default: new_default_value
|
|
#
|
|
# The upcoming change name is used to determine if the override is active.
|
|
# The new default value is used to override the default value for the site setting.
|
|
def upcoming_change_default_overrides
|
|
@upcoming_change_default_overrides ||= {}
|
|
end
|
|
|
|
def hidden_settings_provider
|
|
@hidden_settings_provider ||= SiteSettings::HiddenProvider.new
|
|
end
|
|
|
|
def hidden_settings
|
|
hidden_settings_provider.all
|
|
end
|
|
|
|
def refresh_settings
|
|
@refresh_settings ||= [:default_locale]
|
|
end
|
|
|
|
def client_settings
|
|
@client_settings ||= [:default_locale]
|
|
end
|
|
|
|
def previews
|
|
@previews ||= {}
|
|
end
|
|
|
|
def secret_settings
|
|
@secret_settings ||= Set.new
|
|
end
|
|
|
|
def plugins
|
|
@plugins ||= {}
|
|
end
|
|
|
|
def load_settings(file, plugin: nil)
|
|
SiteSettings::YamlLoader
|
|
.new(file)
|
|
.load do |category, name, default, opts|
|
|
setting(name, default, opts.merge(category: category, plugin: plugin))
|
|
end
|
|
end
|
|
|
|
def settings_hash
|
|
result = {}
|
|
|
|
defaults.all.keys.each do |s|
|
|
next if themeable[s]
|
|
|
|
result[s] = if deprecated_settings.include?(s.to_s)
|
|
public_send(s, warn: false).to_s
|
|
else
|
|
public_send(s).to_s
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def deprecated_settings
|
|
@deprecated_settings ||= SiteSettings::DeprecatedSettings::SETTINGS.map(&:first).to_set
|
|
end
|
|
|
|
def deprecated_setting_alias(setting_name)
|
|
SiteSettings::DeprecatedSettings::SETTINGS
|
|
.find { |setting| setting.second.to_s == setting_name.to_s }
|
|
&.first
|
|
end
|
|
|
|
def theme_site_settings_json(theme_id)
|
|
key = SiteSettingExtension.theme_site_settings_cache_key(theme_id)
|
|
json =
|
|
Discourse
|
|
.cache
|
|
.fetch(key, expires_in: 30.minutes) { theme_site_settings_json_uncached(theme_id) }
|
|
Rails.logger.error("Nil theme_site_settings_json from the cache for '#{key}'") if json.nil?
|
|
json || ""
|
|
rescue => e
|
|
Rails.logger.error("Error while retrieving theme_site_settings_json: #{e.message}")
|
|
""
|
|
end
|
|
|
|
def setting_metadata_hash(setting)
|
|
{
|
|
setting:,
|
|
default: SiteSetting.defaults[setting],
|
|
description: SiteSetting.description(setting),
|
|
humanized_name: humanized_names(setting),
|
|
}.merge(type_supervisor.type_hash(setting))
|
|
end
|
|
|
|
def themeable_site_settings
|
|
themeable.select { |_, value| value }.keys.sort
|
|
end
|
|
|
|
def upcoming_change_site_settings
|
|
upcoming_change_metadata.keys.sort
|
|
end
|
|
|
|
def client_settings_json
|
|
key = SiteSettingExtension.client_settings_cache_key
|
|
json = Discourse.cache.fetch(key, expires_in: 30.minutes) { client_settings_json_uncached }
|
|
Rails.logger.error("Nil client_settings_json from the cache for '#{key}'") if json.nil?
|
|
json || ""
|
|
rescue => e
|
|
Rails.logger.error("Error while retrieving client_settings_json: #{e.message}")
|
|
""
|
|
end
|
|
|
|
def client_settings_json_uncached(return_defaults: false)
|
|
uncached_json =
|
|
@client_settings.filter_map do |name|
|
|
# Themeable site settings require a theme ID, which we do not always
|
|
# have when loading client site settings. They are excluded here,
|
|
# to get them use theme_site_settings_json(:theme_id)
|
|
next if themeable[name]
|
|
|
|
value =
|
|
if return_defaults
|
|
SiteSetting.defaults[name]
|
|
elsif deprecated_settings.include?(name.to_s)
|
|
public_send(name, warn: false)
|
|
else
|
|
public_send(name)
|
|
end
|
|
|
|
type = type_supervisor.get_type(name)
|
|
if type == :upload
|
|
value = value.to_s
|
|
elsif type == :uploaded_image_list && value.present?
|
|
value = value.map(&:to_s).join("|")
|
|
end
|
|
|
|
[name, value]
|
|
end
|
|
MultiJson.dump(Hash[uncached_json])
|
|
rescue => err
|
|
# If something goes wrong here we really need to be aware of it in tests.
|
|
raise err if Rails.env.test?
|
|
|
|
Rails.logger.error("Error while generating client_settings_json_uncached: #{err.message}")
|
|
raise
|
|
end
|
|
|
|
def theme_site_settings_json_uncached(theme_id)
|
|
begin
|
|
# There are a few legit scenarios where the current
|
|
# theme ID may be blank, such as safe mode. In this
|
|
# case it will be better to return default site setting
|
|
# values rather than to cause random/undefined behaviour
|
|
# in the UI.
|
|
if theme_id.blank?
|
|
MultiJson.dump(ThemeSiteSetting.generate_defaults_map)
|
|
else
|
|
MultiJson.dump(theme_site_settings[theme_id])
|
|
end
|
|
rescue => err
|
|
# If something goes wrong here we really need to be aware of it in tests.
|
|
raise err if Rails.env.test?
|
|
|
|
Rails.logger.error(
|
|
"Error while generating theme_site_settings_json_uncached for theme ID #{theme_id}: #{err.message}",
|
|
)
|
|
nil
|
|
end
|
|
end
|
|
|
|
def all_settings(
|
|
include_hidden: false,
|
|
include_locale_setting: true,
|
|
only_overridden: false,
|
|
basic_attributes: false,
|
|
only_upcoming_changes: false,
|
|
filter_categories: nil,
|
|
filter_plugin: nil,
|
|
filter_names: nil,
|
|
filter_allowed_hidden: nil,
|
|
filter_area: nil
|
|
)
|
|
locale_setting_hash = {
|
|
setting: :default_locale,
|
|
humanized_name: humanized_names(:default_locale),
|
|
default: SiteSettings::DefaultsProvider::DEFAULT_LOCALE,
|
|
category: "required",
|
|
primary_area: "localization",
|
|
description: description(:default_locale),
|
|
type: SiteSetting.types[SiteSetting.types[:locale_enum]],
|
|
preview: nil,
|
|
value: self.default_locale,
|
|
valid_values: LocaleSiteSetting.values,
|
|
translate_names: LocaleSiteSetting.translate_names?,
|
|
}
|
|
|
|
include_locale_setting = false if filter_categories.present? || filter_plugin.present?
|
|
|
|
# There is a hidden_site_settings modifier in HiddenSettingsProvider
|
|
# that can cause perf overhead, so instead of calling hidden_settings
|
|
# in a loop, we call it once here.
|
|
current_hidden_settings = hidden_settings
|
|
|
|
defaults
|
|
.all(default_locale)
|
|
.reject do |setting_name, _|
|
|
plugins[setting_name] && !Discourse.plugins_by_name[plugins[setting_name]].configurable?
|
|
end
|
|
.select do |setting_name, _|
|
|
is_hidden = current_hidden_settings.include?(setting_name)
|
|
|
|
next true if !is_hidden
|
|
next false if !include_hidden
|
|
next true if filter_allowed_hidden.nil?
|
|
|
|
filter_allowed_hidden.include?(setting_name)
|
|
end
|
|
.select do |setting_name, _|
|
|
if filter_categories && filter_categories.any?
|
|
filter_categories.include?(categories[setting_name])
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.select do |setting_name, _|
|
|
if filter_area
|
|
Array.wrap(areas[setting_name]).include?(filter_area)
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.select do |setting_name, _|
|
|
if filter_plugin
|
|
plugins[setting_name] == filter_plugin
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.select do |setting_name, _|
|
|
if only_upcoming_changes
|
|
UpcomingChanges.exists?(setting_name) &&
|
|
UpcomingChanges.change_status(setting_name) != :conceptual
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.map do |s, v|
|
|
type_hash = type_supervisor.type_hash(s)
|
|
|
|
# NOTE: This can be overridden by an upcoming change, see .refresh!() for the
|
|
# logic here, and .setting() for where we load the upcoming change default overrides
|
|
# from yaml.
|
|
default = defaults.get(s, default_locale).to_s
|
|
|
|
# Has the old default, new default, and upcoming change name. Will only
|
|
# have these values if there is an override and the upcoming change is enabled.
|
|
upcoming_change_default_override_metadata = defaults.upcoming_change_override_metadata(s)
|
|
|
|
if themeable[s]
|
|
value = public_send(s, { theme_id: SiteSetting.default_theme_id })
|
|
else
|
|
value = public_send(s)
|
|
end
|
|
|
|
value = value.map(&:to_s).join("|") if type_hash[:type].to_s == "uploaded_image_list"
|
|
|
|
if type_hash[:type].to_s == "upload" && default.to_i < Upload::SEEDED_ID_THRESHOLD
|
|
default = default_uploads[default.to_i]
|
|
end
|
|
|
|
# For upload type settings, include the upload metadata
|
|
# public_send returns the Upload object directly (not a URL string)
|
|
upload_metadata = nil
|
|
if type_hash[:type].to_s == "upload" && value.is_a?(Upload)
|
|
upload_metadata = {
|
|
original_filename: value.original_filename,
|
|
human_filesize: value.human_filesize,
|
|
width: value.width,
|
|
height: value.height,
|
|
}
|
|
end
|
|
|
|
# For uploads nested in objects type, hydrate upload IDs to URLs
|
|
if type_hash[:type].to_s == "objects" && type_hash[:schema]
|
|
parsed_value = JSON.parse(value)
|
|
value = hydrate_uploads_in_objects(parsed_value, type_hash[:schema])
|
|
end
|
|
|
|
opts = {
|
|
setting: s,
|
|
humanized_name: humanized_names(s),
|
|
description: description(s),
|
|
keywords: keywords(s),
|
|
category: categories[s],
|
|
primary_area: areas[s]&.first,
|
|
}
|
|
|
|
if !basic_attributes
|
|
# For objects type, serialize as JSON
|
|
serialized_value =
|
|
if type_hash[:type].to_s == "objects"
|
|
value.to_json
|
|
else
|
|
value.to_s
|
|
end
|
|
|
|
opts_data = {
|
|
default:,
|
|
value: serialized_value,
|
|
preview: previews[s],
|
|
secret: secret_settings.include?(s),
|
|
placeholder: placeholder(s),
|
|
mandatory_values: mandatory_values[s],
|
|
disallowed_groups: disallowed_groups[s],
|
|
requires_confirmation: requires_confirmation_settings[s],
|
|
upcoming_change: only_upcoming_changes ? upcoming_change_metadata[s] : nil,
|
|
themeable: themeable[s],
|
|
}
|
|
|
|
if depends_on = type_supervisor.dependencies[s]
|
|
opts_data[:depends_on] = depends_on
|
|
opts_data[:depends_on_humanized_names] = depends_on.map { |dep| humanized_names(dep) }
|
|
opts_data[:depends_behavior] = type_supervisor.dependencies.behaviors[s]
|
|
end
|
|
|
|
if upcoming_change_default_override_metadata
|
|
opts_data[
|
|
:upcoming_change_default_override_metadata
|
|
] = upcoming_change_default_override_metadata
|
|
end
|
|
|
|
opts.merge!(opts_data.merge(type_hash))
|
|
end
|
|
|
|
opts[:plugin] = plugins[s] if plugins[s]
|
|
opts[:upload] = upload_metadata if upload_metadata
|
|
|
|
DiscoursePluginRegistry.apply_modifier(:site_setting_result, opts)
|
|
end
|
|
.select do |setting|
|
|
if only_overridden
|
|
setting[:value] != setting[:default]
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.select do |setting|
|
|
if filter_names
|
|
filter_names.include?(setting[:setting].to_s)
|
|
else
|
|
true
|
|
end
|
|
end
|
|
.unshift(include_locale_setting && !only_overridden ? locale_setting_hash : nil)
|
|
.compact
|
|
end
|
|
|
|
def self.client_settings_cache_key
|
|
# NOTE: we use the git version in the key to ensure
|
|
# that we don't end up caching the incorrect version
|
|
# in cases where we are cycling unicorns
|
|
"client_settings_json_#{Discourse.git_version}"
|
|
end
|
|
|
|
def self.theme_site_settings_cache_key(theme_id)
|
|
theme_id = "notheme" if theme_id.blank?
|
|
|
|
# NOTE: we use the git version in the key to ensure
|
|
# that we don't end up caching the incorrect version
|
|
# in cases where we are cycling unicorns
|
|
"theme_site_settings_json_#{theme_id}__#{Discourse.git_version}"
|
|
end
|
|
|
|
# Merges the provider values of site settings (whether it be from the DB or wherever)
|
|
# and theme site settings with the default values of those settings, also taking into
|
|
# account shadowed site settings and upcoming change behaviour.
|
|
def refresh!(refresh_site_settings: true, refresh_theme_site_settings: true)
|
|
mutex.synchronize do
|
|
ensure_listen_for_changes
|
|
|
|
if refresh_site_settings
|
|
new_current_hash = fetch_setting_hash_from_provider
|
|
|
|
# We have to do this because we need to reject any settings that are the
|
|
# same as the default value before we merge the defaults view onto the current
|
|
# hash.
|
|
new_modified = new_current_hash.dup
|
|
|
|
refresh_site_setting_group_ids!
|
|
|
|
defaults_view =
|
|
defaults.all(new_current_hash[:default_locale], include_upcoming_changes_overrides: false)
|
|
|
|
# If the "modified" value is the same as the setting default,
|
|
# this doesn't really count and is more of a quirk on how we
|
|
# store site settings. In this case, we should not consider this
|
|
# setting to be modified.
|
|
#
|
|
# There are two exceptions, both tied to upcoming changes:
|
|
#
|
|
# 1. Upcoming change settings themselves — admins need to be able
|
|
# to manually opt out of an auto-promoted change even when the
|
|
# DB value matches the YAML default.
|
|
#
|
|
# 2. Target settings of an upcoming_change_default_override — the
|
|
# "clean" defaults_view here is computed without overrides
|
|
# applied, so it still reports the original YAML default.
|
|
#
|
|
# When an override is active, the *effective* default is different,
|
|
# and the admin's explicit DB value (even if it equals the
|
|
# original default) is a deliberate opt-out that must survive
|
|
# refresh!. Without this exception, the override loop below
|
|
# would re-clobber the admin's choice.
|
|
new_modified.reject! do |name, val|
|
|
if UpcomingChanges.exists?(name) || upcoming_change_default_overrides.key?(name)
|
|
false
|
|
else
|
|
val.to_s == defaults_view[name].to_s
|
|
end
|
|
end
|
|
|
|
# Merge in the default values for the current locale, since these are
|
|
# precomputed and cached. This ensures that locale-specific and default
|
|
# settings are always available in current_hash.
|
|
new_current_hash = defaults_view.merge!(new_current_hash)
|
|
|
|
shadowed_settings.each { |ss| new_current_hash[ss] = GlobalSetting.public_send(ss) }
|
|
|
|
changes, deletions = diff_hash(new_current_hash, current)
|
|
|
|
changes.each { |name, val| current[name] = val }
|
|
deletions.each { |name, _| current[name] = defaults_view[name] }
|
|
|
|
modified.clear
|
|
modified.merge!(new_modified)
|
|
uploads.clear
|
|
|
|
upcoming_change_default_overrides.each do |setting_name, override|
|
|
if UpcomingChanges.enabled?(override[:upcoming_change])
|
|
defaults.activate_upcoming_change_override(override[:upcoming_change])
|
|
if !setting_modified_from_default?(setting_name)
|
|
current[setting_name] = override[:new_default]
|
|
end
|
|
else
|
|
defaults.deactivate_upcoming_change_override(override[:upcoming_change])
|
|
end
|
|
end
|
|
end
|
|
|
|
refresh_theme_site_settings! if refresh_theme_site_settings
|
|
|
|
clear_cache!(
|
|
expire_theme_site_setting_cache:
|
|
ThemeSiteSetting.can_access_db? && refresh_theme_site_settings,
|
|
)
|
|
end
|
|
end
|
|
|
|
# Whether an admin or something else has changed the site setting from its
|
|
# default value, writing a new value to the database.
|
|
#
|
|
# Note that if the DB value is the same as the default value, this will return false.
|
|
def setting_modified_from_default?(setting_name)
|
|
modified.key?(setting_name)
|
|
end
|
|
|
|
def refresh_site_setting_group_ids!
|
|
new_site_setting_group_ids_hash = SiteSettingGroup.generate_setting_group_map
|
|
site_setting_group_id_changes, site_setting_group_id_deletions =
|
|
diff_hash(new_site_setting_group_ids_hash, site_setting_group_ids)
|
|
|
|
site_setting_group_id_changes.each { |name, val| site_setting_group_ids[name] = val }
|
|
site_setting_group_id_deletions.each { |name, _| site_setting_group_ids.delete(name) }
|
|
end
|
|
|
|
def refresh_theme_site_settings!
|
|
new_theme_site_settings = ThemeSiteSetting.generate_theme_map
|
|
|
|
theme_site_setting_changes, theme_site_setting_deletions =
|
|
diff_hash(new_theme_site_settings, theme_site_settings)
|
|
|
|
theme_site_setting_changes.each do |theme_id, settings|
|
|
theme_site_settings[theme_id] ||= {}
|
|
theme_site_settings[theme_id].merge!(settings)
|
|
end
|
|
|
|
theme_site_setting_deletions.each { |theme_id, _| theme_site_settings.delete(theme_id) }
|
|
end
|
|
|
|
SITE_SETTINGS_CHANNEL = "/site_settings"
|
|
CLIENT_SETTINGS_CHANNEL = "/client_settings"
|
|
|
|
def ensure_listen_for_changes
|
|
return if @listen_for_changes == false
|
|
|
|
unless @subscribed
|
|
MessageBus.subscribe(SITE_SETTINGS_CHANNEL) do |message|
|
|
process_message(message) if message.data["process"] != process_id
|
|
end
|
|
|
|
@subscribed = true
|
|
end
|
|
end
|
|
|
|
def process_message(message)
|
|
begin
|
|
MessageBus.on_connect.call(message.site_id)
|
|
refresh!
|
|
ensure
|
|
MessageBus.on_disconnect.call(message.site_id)
|
|
end
|
|
end
|
|
|
|
def process_id
|
|
@process_id ||= SecureRandom.uuid
|
|
end
|
|
|
|
def after_fork
|
|
@process_id = nil
|
|
ensure_listen_for_changes
|
|
end
|
|
|
|
def raise_invalid_setting_access(setting_name)
|
|
raise SiteSettingExtension::InvalidSettingAccess.new(
|
|
"#{setting_name} cannot be changed like this because it is a themeable setting. Instead, use the ThemeSiteSettingManager service to manage themeable site settings.",
|
|
)
|
|
end
|
|
|
|
##
|
|
# Removes an override for a setting, reverting it to the default value.
|
|
# This method is only called manually usually, more often than not
|
|
# setting overrides are removed in database migrations.
|
|
#
|
|
# Here we also handle notifying the UI of the change in the case
|
|
# of theme site settings and clearing relevant caches, and triggering
|
|
# server-side events for changed settings.
|
|
#
|
|
# Themeable site settings cannot be removed this way, they must be
|
|
# changed via the ThemeSiteSetting model.
|
|
#
|
|
# @param name [Symbol] the name of the setting
|
|
# @param val [Any] the value to set
|
|
def remove_override!(name)
|
|
raise_invalid_setting_access(name) if themeable[name]
|
|
|
|
old_val = current[name]
|
|
provider.destroy(name)
|
|
current[name] = defaults.get(name, default_locale)
|
|
modified.delete(name)
|
|
|
|
if UpcomingChanges.exists?(name)
|
|
apply_upcoming_change_default_overrides_for!(
|
|
name,
|
|
upcoming_change_enabled: UpcomingChanges.enabled?(name),
|
|
)
|
|
end
|
|
|
|
return if current[name] == old_val
|
|
|
|
clear_uploads_cache(name)
|
|
clear_cache!
|
|
if old_val != current[name]
|
|
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name])
|
|
end
|
|
end
|
|
|
|
##
|
|
# Adds an override, which is to say a database entry for the setting
|
|
# instead of using the default.
|
|
#
|
|
# The `set`, `set_and_log`, and `setting_name=` methods all call
|
|
# this method. Its opposite is remove_override!.
|
|
#
|
|
# Here we also handle notifying the UI of the change in the case
|
|
# of theme site settings and clearing relevant caches, and triggering
|
|
# server-side events for changed settings.
|
|
#
|
|
# Themeable site settings cannot be changed this way, they must be
|
|
# changed via the ThemeSiteSetting model.
|
|
#
|
|
# @param name [Symbol] the name of the setting
|
|
# @param val [Any] the value to set
|
|
#
|
|
# @example
|
|
# SiteSetting.add_override!(:site_description, "My awesome forum")
|
|
#
|
|
# @raise [SiteSettingExtension::InvalidSettingAccess] if the setting is themeable
|
|
# (themeable settings must be changed via ThemeSiteSetting model)
|
|
#
|
|
# @note When called from the Rails console, this method automatically logs the change
|
|
# with the system user.
|
|
#
|
|
# @see remove_override! for removing an override and reverting to default value
|
|
def add_override!(name, val)
|
|
raise_invalid_setting_access(name) if themeable[name]
|
|
|
|
old_val = current[name]
|
|
val, type = type_supervisor.to_db_value(name, val)
|
|
|
|
sanitize_override = val.is_a?(String) && client_settings.include?(name)
|
|
|
|
sanitized_val = sanitize_override ? sanitize_field(val) : val
|
|
|
|
if mandatory_values[name.to_sym]
|
|
sanitized_val =
|
|
(mandatory_values[name.to_sym].split("|") | sanitized_val.to_s.split("|")).join("|")
|
|
end
|
|
|
|
if disallowed_groups[name.to_sym]
|
|
disallowed = disallowed_groups[name.to_sym].split("|")
|
|
sanitized_val = sanitized_val.to_s.split("|").reject { |v| disallowed.include?(v) }.join("|")
|
|
end
|
|
|
|
provider.save(name, sanitized_val, type)
|
|
current[name] = type_supervisor.to_rb_value(name, sanitized_val)
|
|
modified[name] = current[name]
|
|
|
|
if UpcomingChanges.exists?(name)
|
|
apply_upcoming_change_default_overrides_for!(name, upcoming_change_enabled: current[name])
|
|
end
|
|
|
|
return if current[name] == old_val
|
|
|
|
clear_uploads_cache(name)
|
|
notify_clients!(name) if client_settings.include?(name)
|
|
clear_cache!
|
|
|
|
if defined?(Rails::Console)
|
|
details = "Updated via Rails console"
|
|
details = DiscoursePluginRegistry.apply_modifier(:site_setting_log_details, details)
|
|
log(name, val, old_val, Discourse.system_user, details)
|
|
end
|
|
|
|
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name])
|
|
end
|
|
|
|
# Updates a theme-specific site setting value in memory and notifies observers.
|
|
#
|
|
# This method is used to change site settings that are marked as "themeable",
|
|
# which means they can have different values per theme. Unlike `add_override!`,
|
|
# the database isn't touched here.
|
|
#
|
|
# @param theme_id [Integer] The ID of the theme to update the setting for
|
|
# @param name [String, Symbol] The name of the site setting to change
|
|
# @param val [Object] The new "ruby" value for the site setting
|
|
#
|
|
# @example
|
|
# SiteSetting.change_themeable_site_setting(5, "enable_welcome_banner", false)
|
|
#
|
|
# @note Unlike regular site settings which use add_override!, themeable settings
|
|
# should be changed via the ThemeSiteSettingManager service.
|
|
#
|
|
# @see ThemeSiteSettingManager service for the higher-level implementation that handles
|
|
# database persistence and logging.
|
|
def change_themeable_site_setting(theme_id, name, val)
|
|
name = name.to_sym
|
|
|
|
theme_site_settings[theme_id] ||= {}
|
|
old_val = theme_site_settings[theme_id][name]
|
|
theme_site_settings[theme_id][name] = val
|
|
|
|
notify_clients!(name, theme_id: theme_id) if client_settings.include?(name)
|
|
notify_changed!
|
|
|
|
clear_cache!(expire_theme_site_setting_cache: true)
|
|
|
|
DiscourseEvent.trigger(:theme_site_setting_changed, name, old_val, val)
|
|
end
|
|
|
|
# NOTE: This will not refresh the current process' site settings, only other processes
|
|
# that are listening for changes. We check if the current process_id is != to the message
|
|
# process ID before refreshing in process_message.
|
|
#
|
|
# If you need to refresh the current process as well, call refresh! (or another
|
|
# method to update caches) directly.
|
|
def notify_changed!
|
|
MessageBus.publish(SITE_SETTINGS_CHANNEL, process: process_id)
|
|
end
|
|
|
|
def notify_clients!(name, scoped_to = nil)
|
|
# Group-based upcoming changes cannot update clients, because we need
|
|
# to know a user to determine if the change is active for them.
|
|
#
|
|
# This is the same limitation that group-based site settings have --
|
|
# we cannot determine the full groups of a user on the client side,
|
|
# so we only use these in the CurrentUserSerializer to send down an
|
|
# attribute. Users will get the new value on page reload.
|
|
#
|
|
# If the upcoming change is not group-based then it's safe to just
|
|
# use the underlying site setting value.
|
|
if upcoming_change_site_settings.include?(name.to_sym) && UpcomingChanges.has_groups?(name)
|
|
return
|
|
end
|
|
|
|
MessageBus.publish(
|
|
CLIENT_SETTINGS_CHANNEL,
|
|
name: name,
|
|
# default_locale is a special case, it is not themeable and we define
|
|
# a custom getter for it, so we can just use the normal getter
|
|
value:
|
|
name.to_s == "default_locale" ? self.public_send(name) : self.public_send(name, scoped_to),
|
|
scoped_to: scoped_to,
|
|
)
|
|
end
|
|
|
|
def requires_refresh?(name)
|
|
refresh_settings.include?(name.to_sym)
|
|
end
|
|
|
|
HOSTNAME_SETTINGS = %w[
|
|
disabled_image_download_domains
|
|
blocked_onebox_domains
|
|
exclude_rel_nofollow_domains
|
|
blocked_email_domains
|
|
allowed_email_domains
|
|
allowed_spam_host_domains
|
|
]
|
|
|
|
def filter_value(name, value)
|
|
if HOSTNAME_SETTINGS.include?(name)
|
|
value
|
|
.split("|")
|
|
.map do |url|
|
|
url.strip!
|
|
get_hostname(url)
|
|
end
|
|
.compact
|
|
.uniq
|
|
.join("|")
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
def set(name, value, options = nil)
|
|
if has_setting?(name)
|
|
raise_invalid_setting_access(name) if themeable[name]
|
|
|
|
value = filter_value(name, value)
|
|
if options
|
|
self.public_send("#{name}=", value, options)
|
|
else
|
|
self.public_send("#{name}=", value)
|
|
end
|
|
Discourse.request_refresh! if requires_refresh?(name)
|
|
else
|
|
raise Discourse::InvalidParameters.new(
|
|
"Either no setting named '#{name}' exists or value provided is invalid",
|
|
)
|
|
end
|
|
end
|
|
|
|
def set_and_log(name, value, user = Discourse.system_user, detailed_message = nil)
|
|
if has_setting?(name)
|
|
raise_invalid_setting_access(name) if themeable[name]
|
|
|
|
prev_value = public_send(name)
|
|
return if prev_value == value
|
|
set(name, value)
|
|
# Logging via the rails console is already handled in add_override!
|
|
log(name, value, prev_value, user, detailed_message) unless defined?(Rails::Console)
|
|
SiteSettingChangeResult.new(prev_value, public_send(name))
|
|
else
|
|
raise Discourse::InvalidParameters.new(
|
|
I18n.t("errors.site_settings.invalid_site_setting", name: name),
|
|
)
|
|
end
|
|
end
|
|
|
|
def log(name, value, prev_value, user = Discourse.system_user, detailed_message = nil)
|
|
return if hidden_settings.include?(name.to_sym)
|
|
value = prev_value = "[FILTERED]" if secret_settings.include?(name.to_sym)
|
|
StaffActionLogger.new(user).log_site_setting_change(
|
|
name,
|
|
prev_value,
|
|
value,
|
|
{ details: detailed_message }.compact_blank,
|
|
)
|
|
end
|
|
|
|
def get(name, scoped_to = nil)
|
|
if has_setting?(name)
|
|
if themeable[name]
|
|
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
|
raise SiteSettingExtension::InvalidSettingAccess.new(
|
|
"#{name} requires a theme_id because it is themeable",
|
|
)
|
|
else
|
|
self.public_send(name, scoped_to)
|
|
end
|
|
else
|
|
self.public_send(name)
|
|
end
|
|
else
|
|
raise Discourse::InvalidParameters.new(
|
|
I18n.t("errors.site_settings.invalid_site_setting", name: name),
|
|
)
|
|
end
|
|
end
|
|
|
|
if defined?(Rails::Console)
|
|
# Convenience method for debugging site setting issues
|
|
# Returns a hash with information about a specific setting
|
|
def info(name)
|
|
{
|
|
resolved_value: get(name),
|
|
default_value: defaults[name],
|
|
global_override: GlobalSetting.respond_to?(name) ? GlobalSetting.public_send(name) : nil,
|
|
database_value: provider.find(name)&.value,
|
|
refresh?: refresh_settings.include?(name),
|
|
client?: client_settings.include?(name),
|
|
secret?: secret_settings.include?(name),
|
|
}
|
|
end
|
|
end
|
|
|
|
def valid_areas
|
|
Set.new(SiteSetting::VALID_AREAS | DiscoursePluginRegistry.site_setting_areas.to_a)
|
|
end
|
|
|
|
protected
|
|
|
|
def clear_cache!(expire_theme_site_setting_cache: false)
|
|
Discourse.cache.delete(SiteSettingExtension.client_settings_cache_key)
|
|
Theme.expire_site_setting_cache! if expire_theme_site_setting_cache
|
|
Site.clear_anon_cache!
|
|
end
|
|
|
|
def diff_hash(new_hash, old)
|
|
changes = []
|
|
deletions = []
|
|
|
|
new_hash.each do |name, value|
|
|
changes << [name, value] if !old.has_key?(name) || old[name] != value
|
|
end
|
|
|
|
old.each { |name, value| deletions << [name, value] unless new_hash.has_key?(name) }
|
|
|
|
[changes, deletions]
|
|
end
|
|
|
|
def setup_shadowed_methods(name, value)
|
|
clean_name = name.to_s.sub("?", "").to_sym
|
|
|
|
define_singleton_method clean_name do |scoped_to = nil|
|
|
value
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}?" do |scoped_to = nil|
|
|
value
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}=" do |val|
|
|
if value != val
|
|
Rails.logger.warn(
|
|
"An attempt was to change #{clean_name} SiteSetting to #{val} however it is shadowed so this will be ignored!",
|
|
)
|
|
end
|
|
nil
|
|
end
|
|
end
|
|
|
|
def setup_methods(name)
|
|
clean_name = name.to_s.sub("?", "").to_sym
|
|
|
|
if type_supervisor.get_type(name) == :uploaded_image_list
|
|
define_singleton_method clean_name do |scoped_to = nil|
|
|
if themeable[clean_name]
|
|
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
|
raise SiteSettingExtension::InvalidSettingAccess.new(
|
|
"#{clean_name} requires a theme_id because it is themeable",
|
|
)
|
|
end
|
|
end
|
|
|
|
uploads_list = uploads[name]
|
|
return uploads_list if uploads_list
|
|
|
|
if (value = current[name]).nil?
|
|
refresh!
|
|
value = current[name]
|
|
end
|
|
|
|
return [] if value.empty?
|
|
|
|
value = value.split("|").map(&:to_i)
|
|
uploads_list = Upload.where(id: value).to_a
|
|
uploads[name] = uploads_list if uploads_list
|
|
end
|
|
elsif type_supervisor.get_type(name) == :upload
|
|
define_singleton_method clean_name do |scoped_to = nil|
|
|
if themeable[clean_name]
|
|
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
|
raise SiteSettingExtension::InvalidSettingAccess.new(
|
|
"#{clean_name} requires a theme_id because it is themeable",
|
|
)
|
|
end
|
|
end
|
|
|
|
upload = uploads[name]
|
|
return upload if upload
|
|
|
|
if (value = current[name]).nil?
|
|
refresh!
|
|
value = current[name]
|
|
end
|
|
|
|
value = value.to_i
|
|
|
|
if value != Upload::SEEDED_ID_THRESHOLD
|
|
upload = Upload.find_by(id: value)
|
|
uploads[name] = upload if upload
|
|
end
|
|
end
|
|
else
|
|
define_singleton_method clean_name do |scoped_to = nil|
|
|
if themeable[clean_name]
|
|
if scoped_to.nil? || !scoped_to.key?(:theme_id) || scoped_to[:theme_id].nil?
|
|
raise SiteSettingExtension::InvalidSettingAccess.new(
|
|
"#{clean_name} requires a theme_id because it is themeable",
|
|
)
|
|
end
|
|
|
|
# If the theme hasn't overridden any theme site settings (or changed defaults)
|
|
# then we will just fall back further down bellow to the current site setting value.
|
|
settings_overridden_for_theme = theme_site_settings[scoped_to[:theme_id]]
|
|
if settings_overridden_for_theme && settings_overridden_for_theme.key?(clean_name)
|
|
return settings_overridden_for_theme[clean_name]
|
|
end
|
|
end
|
|
|
|
if plugins[name]
|
|
plugin = Discourse.plugins_by_name[plugins[name]]
|
|
return false if !plugin.configurable? && plugin.enabled_site_setting == name
|
|
end
|
|
|
|
refresh! if current[name].nil?
|
|
|
|
value =
|
|
if UpcomingChanges.exists?(name)
|
|
UpcomingChanges.enabled?(name)
|
|
else
|
|
# Will either be the value the admin changed the site setting to
|
|
# in the DB/provider, or the default value, which may also be affected
|
|
# by an upcoming change.
|
|
current[name]
|
|
end
|
|
|
|
if mandatory_values[name]
|
|
return (mandatory_values[name].split("|") | value.to_s.split("|")).join("|")
|
|
end
|
|
|
|
value
|
|
end
|
|
end
|
|
|
|
# Any group_list or category_list setting will have a getter defined with _map
|
|
# on the end, e.g. personal_message_enabled_groups_map, to avoid having to
|
|
# manually split and convert to integer for these settings.
|
|
if %i[group_list category_list].include?(type_supervisor.get_type(name))
|
|
define_singleton_method("#{clean_name}_map") do
|
|
self.public_send(clean_name).to_s.split("|").map(&:to_i)
|
|
end
|
|
end
|
|
|
|
# Upcoming change settings have a supplemental array of group IDs that are used to opt-in
|
|
# certain groups to the change early. We use the data from SiteSettingGroup to define
|
|
# a getter with _groups_map on the end, e.g. allow_unlimited_uploads_groups_map,
|
|
# to avoid having to manually split and convert to integer for these settings.
|
|
if UpcomingChanges.exists?(name) && type_supervisor.get_type(name) == :bool
|
|
define_singleton_method("#{clean_name}_groups_map") do
|
|
site_setting_group_ids[name].presence || []
|
|
end
|
|
end
|
|
|
|
# Same logic as above for other list type settings, with the caveat that normal
|
|
# list settings are not necessarily integers, so we just want to handle the splitting.
|
|
if %i[list emoji_list tag_list].include?(type_supervisor.get_type(name))
|
|
list_type = type_supervisor.get_list_type(name)
|
|
|
|
if %w[simple compact].include?(list_type) || list_type.nil?
|
|
define_singleton_method("#{clean_name}_map") do |scoped_to = nil|
|
|
self.public_send(clean_name, scoped_to).to_s.split("|")
|
|
end
|
|
end
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}?" do |scoped_to = nil|
|
|
self.public_send(clean_name, scoped_to)
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}=" do |val|
|
|
raise_invalid_setting_access(clean_name) if themeable[clean_name]
|
|
|
|
add_override!(name, val)
|
|
end
|
|
end
|
|
|
|
def get_hostname(url)
|
|
host =
|
|
begin
|
|
URI.parse(url)&.host
|
|
rescue URI::Error
|
|
nil
|
|
end
|
|
|
|
host ||=
|
|
begin
|
|
URI.parse("http://#{url}")&.host
|
|
rescue URI::Error
|
|
nil
|
|
end
|
|
|
|
host.presence || url
|
|
end
|
|
|
|
private
|
|
|
|
# Gets all of the site setting values from the provider (DbProvider (most
|
|
# common), TestProvider, LocalProcessProvider etc.) and returns a hash of
|
|
# setting names to values based on the type of the setting.
|
|
#
|
|
# @return [Hash] a hash of setting names to values based on the type of the setting
|
|
#
|
|
# @example
|
|
# {
|
|
# enable_mobile_theme: true,
|
|
# topics_per_period_in_top_page: 50,
|
|
# title: "My awesome forum"
|
|
# }
|
|
def fetch_setting_hash_from_provider
|
|
Hash[
|
|
*(
|
|
provider
|
|
.all
|
|
.map do |setting|
|
|
[
|
|
setting.name.to_sym,
|
|
type_supervisor.to_rb_value(setting.name, setting.value, setting.data_type),
|
|
]
|
|
end
|
|
.flatten
|
|
)
|
|
]
|
|
end
|
|
|
|
def apply_upcoming_change_default_overrides_for!(name, upcoming_change_enabled:)
|
|
if upcoming_change_enabled
|
|
defaults.activate_upcoming_change_override(name)
|
|
else
|
|
defaults.deactivate_upcoming_change_override(name)
|
|
end
|
|
|
|
# Similar to what we do in refresh!, but we are being more reactive here.
|
|
# If the admin enables/disables an upcoming change we need to make sure
|
|
# the overridden default is used, otherwise we fall back to the old default,
|
|
# but only if the admin hasn't modified the target setting themselves.
|
|
upcoming_change_default_overrides.each do |setting_name, override|
|
|
next if override[:upcoming_change] != name.to_sym
|
|
next if setting_modified_from_default?(setting_name)
|
|
|
|
if upcoming_change_enabled
|
|
current[setting_name] = override[:new_default]
|
|
else
|
|
current[setting_name] = defaults.get(setting_name, default_locale)
|
|
end
|
|
end
|
|
end
|
|
|
|
def setting(name_arg, default = nil, opts = {})
|
|
name = name_arg.to_sym
|
|
|
|
if name == :default_locale
|
|
raise Discourse::InvalidParameters.new(
|
|
"Other settings depend on default locale, you can not configure it like this",
|
|
)
|
|
end
|
|
|
|
shadowed_val = nil
|
|
|
|
mutex.synchronize do
|
|
defaults.load_setting(name, default, opts.delete(:locale_default))
|
|
|
|
mandatory_values[name] = opts[:mandatory_values] if opts[:mandatory_values]
|
|
disallowed_groups[name] = opts[:disallowed_groups] if opts[:disallowed_groups]
|
|
|
|
requires_confirmation_settings[name] = (
|
|
if SiteSettings::TypeSupervisor::REQUIRES_CONFIRMATION_TYPES.values.include?(
|
|
opts[:requires_confirmation],
|
|
)
|
|
opts[:requires_confirmation]
|
|
end
|
|
)
|
|
|
|
if opts[:upcoming_change]
|
|
upcoming_change_metadata[name] ||= {}
|
|
impact_type, impact_role = opts[:upcoming_change][:impact].split(",")
|
|
upcoming_change_metadata[name].merge!(
|
|
**opts[:upcoming_change].except(:impact),
|
|
impact_type: impact_type,
|
|
impact_role: impact_role,
|
|
status: opts[:upcoming_change][:status].to_sym,
|
|
)
|
|
end
|
|
|
|
upcoming_change_default_override = opts[:upcoming_change_default_override]
|
|
if upcoming_change_default_override.present?
|
|
upcoming_change_default_overrides[name] = {
|
|
upcoming_change: upcoming_change_default_override[:upcoming_change].to_sym,
|
|
new_default: upcoming_change_default_override[:new_default],
|
|
}
|
|
end
|
|
|
|
categories[name] = opts[:category] || :uncategorized
|
|
|
|
themeable[name] = opts[:themeable] ? true : false
|
|
|
|
if opts[:area]
|
|
split_areas = opts[:area].split("|")
|
|
if split_areas.any? { |area| !SiteSetting.valid_areas.include?(area) }
|
|
raise Discourse::InvalidParameters.new(
|
|
"One of the areas in #{opts[:area]} for setting #{name} is invalid, valid areas are: #{SiteSetting.valid_areas.join(", ")}",
|
|
)
|
|
end
|
|
areas[name] = split_areas
|
|
end
|
|
hidden_settings_provider.add_hidden(name) if opts[:hidden]
|
|
|
|
if GlobalSetting.respond_to?(name)
|
|
val = GlobalSetting.public_send(name)
|
|
|
|
unless val.nil? || (val == "")
|
|
shadowed_val = val
|
|
hidden_settings_provider.add_hidden(name)
|
|
shadowed_settings << name
|
|
end
|
|
end
|
|
|
|
refresh_settings << name if opts[:refresh]
|
|
|
|
client_settings << name.to_sym if opts[:client]
|
|
|
|
previews[name] = opts[:preview] if opts[:preview]
|
|
|
|
secret_settings << name if opts[:secret]
|
|
|
|
plugins[name] = opts[:plugin] if opts[:plugin]
|
|
|
|
choices_opts = opts.extract!(*SiteSettings::TypeSupervisor::CONSUMED_OPTS)
|
|
type_supervisor.load_setting(name, choices_opts)
|
|
|
|
if !shadowed_val.nil?
|
|
setup_shadowed_methods(name, shadowed_val)
|
|
else
|
|
setup_methods(name)
|
|
end
|
|
end
|
|
end
|
|
|
|
def default_uploads
|
|
@default_uploads ||= {}
|
|
|
|
@default_uploads[provider.current_site] ||= begin
|
|
Upload.where("id < ?", Upload::SEEDED_ID_THRESHOLD).pluck(:id, :url).to_h
|
|
end
|
|
end
|
|
|
|
def uploads
|
|
@uploads ||= {}
|
|
@uploads[provider.current_site] ||= {}
|
|
end
|
|
|
|
def clear_uploads_cache(name)
|
|
if (
|
|
type_supervisor.get_type(name) == :upload ||
|
|
type_supervisor.get_type(name) == :uploaded_image_list
|
|
) && uploads.has_key?(name)
|
|
uploads.delete(name)
|
|
end
|
|
end
|
|
|
|
def logger
|
|
Rails.logger
|
|
end
|
|
|
|
private
|
|
|
|
def hydrate_uploads_in_objects(objects, schema)
|
|
return objects if objects.blank?
|
|
|
|
upload_ids =
|
|
SchemaSettingsObjectValidator.property_values_of_type(
|
|
schema: schema,
|
|
objects: objects,
|
|
type: "upload",
|
|
)
|
|
|
|
uploads_by_id = Upload.where(id: upload_ids).index_by(&:id)
|
|
objects.map { |obj| hydrate_uploads_in_object(obj, schema[:properties], uploads_by_id) }
|
|
end
|
|
|
|
def hydrate_uploads_in_object(object, properties, uploads_by_id)
|
|
properties.each do |prop_key, prop_value|
|
|
case prop_value[:type]
|
|
when "upload"
|
|
key = prop_key.to_s
|
|
upload_id = object[key]
|
|
upload = uploads_by_id[upload_id]
|
|
object[key] = upload.url if upload
|
|
when "objects"
|
|
nested_objects = object[prop_key.to_s]
|
|
if nested_objects.is_a?(Array)
|
|
nested_objects.each do |nested_obj|
|
|
hydrate_uploads_in_object(nested_obj, prop_value[:schema][:properties], uploads_by_id)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
object
|
|
end
|
|
end
|