discourse/lib/site_setting_extension.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

745 lines
19 KiB
Ruby
Raw Permalink Normal View History

# frozen_string_literal: true
2013-02-05 14:16:51 -05:00
module SiteSettingExtension
include SiteSettings::DeprecatedSettings
include HasSanitizableFields
2013-02-05 14:16:51 -05:00
# 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.blank?
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
2013-02-05 14:16:51 -05:00
def mutex
@mutex ||= Mutex.new
end
def current
@containers ||= {}
@containers[provider.current_site] ||= {}
2013-02-05 14:16:51 -05:00
end
def defaults
@defaults ||= SiteSettings::DefaultsProvider.new(self)
2013-11-13 14:02:47 -05:00
end
def type_supervisor
@type_supervisor ||= SiteSettings::TypeSupervisor.new(defaults)
end
def categories
@categories ||= {}
end
def mandatory_values
@mandatory_values ||= {}
end
def shadowed_settings
@shadowed_settings ||= []
end
def requires_confirmation_settings
@requires_confirmation_settings ||= {}
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
2015-08-27 18:55:19 -04:00
def client_settings
@client_settings ||= [:default_locale]
2015-08-27 18:55:19 -04:00
end
def previews
@previews ||= {}
end
def secret_settings
@secret_settings ||= []
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 deprecated_settings
@deprecated_settings ||= SiteSettings::DeprecatedSettings::SETTINGS.map(&:first).to_set
end
def settings_hash
result = {}
defaults.all.keys.each do |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
2013-02-05 14:16:51 -05:00
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}")
""
2013-02-05 14:16:51 -05:00
end
def client_settings_json_uncached
2018-11-14 02:03:02 -05:00
MultiJson.dump(
Hash[
*@client_settings.flat_map do |name|
value =
if deprecated_settings.include?(name.to_s)
public_send(name, warn: false)
else
public_send(name)
end
type = type_supervisor.get_type(name)
value = value.to_s if type == :upload
value = value.map(&:to_s).join("|") if type == :uploaded_image_list
[name, value]
end
],
2018-11-14 02:03:02 -05:00
)
rescue => e
Rails.logger.error("Error while generating client_settings_json_uncached: #{e.message}")
nil
end
2013-02-05 14:16:51 -05:00
# Retrieve all settings
def all_settings(
include_hidden: false,
include_locale_setting: true,
only_overridden: false,
filter_categories: nil,
filter_plugin: nil,
filter_names: nil,
filter_allowed_hidden: nil
)
locale_setting_hash = {
setting: "default_locale",
default: SiteSettings::DefaultsProvider::DEFAULT_LOCALE,
category: "required",
description: description("default_locale"),
type: SiteSetting.types[SiteSetting.types[: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?
defaults
.all(default_locale)
.reject do |setting_name, _|
plugins[name] && !Discourse.plugins_by_name[plugins[name]].configurable?
end
.select do |setting_name, _|
is_hidden = 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_plugin
plugins[setting_name] == filter_plugin
else
true
end
end
.map do |s, v|
type_hash = type_supervisor.type_hash(s)
default = defaults.get(s, default_locale).to_s
value = public_send(s)
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
2018-11-14 02:03:02 -05:00
opts = {
setting: s,
description: description(s),
keywords: keywords(s),
default: default,
value: value.to_s,
category: categories[s],
preview: previews[s],
secret: secret_settings.include?(s),
placeholder: placeholder(s),
mandatory_values: mandatory_values[s],
requires_confirmation: requires_confirmation_settings[s],
}.merge!(type_hash)
opts[:plugin] = plugins[s] if plugins[s]
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
2013-02-05 14:16:51 -05:00
end
def description(setting)
I18n.t("site_settings.#{setting}", base_path: Discourse.base_path, default: "")
2013-02-05 14:16:51 -05:00
end
def keywords(setting)
Array.wrap(I18n.t("site_settings.keywords.#{setting}", default: ""))
end
def placeholder(setting)
if !I18n.t("site_settings.placeholder.#{setting}", default: "").empty?
I18n.t("site_settings.placeholder.#{setting}")
FEATURE: Automatically generate optimized site metadata icons (#7372) This change automatically resizes icons for various purposes. Admins can now upload `logo` and `logo_small`, and everything else will be auto-generated. Specific icons can still be uploaded separately if required. ## Core - Adds an SiteIconManager module which manages automatic resizing and fallback - Icons are looked up in the OptimizedImage table at runtime, and then cached in Redis. If the resized version is missing for some reason, then most icons will fall back to the original files. Some icons (e.g. PWA Manifest) will return `nil` (because an incorrectly sized icon is worse than a missing icon). - `SiteSetting.site_large_icon_url` will return the optimized version, including any fallback. `SiteSetting.large_icon` continues to return the upload object. This means that (almost) no changes are required in core/plugins to support this new system. - Icons are resized whenever a relevant site setting is changed, and during post-deploy migrations ## Wizard - Allows `requiresRefresh` wizard steps to reload data via AJAX instead of a full page reload - Add placeholders to the **icons** step of the wizard, which automatically update from the "Square Logo" - Various copy updates to support the changes - Remove the "upload-time" resizing for `large_icon`. This is no longer required. ## Site Settings UX - Move logo/icon settings under a new "Branding" tab - Various copy changes to support the changes - Adds placeholder support to the `image-uploader` component - Automatically reloads site settings after saving. This allows setting placeholders to change based on changes to other settings - Upload site settings will be assigned a placeholder if SiteIconManager `responds_to?` an icon of the same name ## Dashboard Warnings - Remove PWA icon and PWA title warnings. Both are now handled automatically. ## Bonus - Updated the sketch logos to use @awesomerobot's new high-res designs
2019-05-01 09:44:45 -04:00
elsif SiteIconManager.respond_to?("#{setting}_url")
SiteIconManager.public_send("#{setting}_url")
end
end
2013-02-05 14:16:51 -05:00
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}"
2013-02-05 14:16:51 -05:00
end
# refresh all the site settings
2013-02-25 11:42:20 -05:00
def refresh!
mutex.synchronize do
2013-02-05 14:16:51 -05:00
ensure_listen_for_changes
new_hash =
Hash[
*(
defaults
.db_all
.map do |s|
[s.name.to_sym, type_supervisor.to_rb_value(s.name, s.value, s.data_type)]
end
.to_a
.flatten
)
]
2013-02-05 14:16:51 -05:00
defaults_view = defaults.all(new_hash[:default_locale])
# add locale default and defaults based on default_locale, cause they are cached
new_hash = defaults_view.merge!(new_hash)
# add shadowed
shadowed_settings.each { |ss| new_hash[ss] = GlobalSetting.public_send(ss) }
changes, deletions = diff_hash(new_hash, current)
changes.each { |name, val| current[name] = val }
deletions.each { |name, _| current[name] = defaults_view[name] }
uploads.clear
clear_cache!
2013-02-05 14:16:51 -05:00
end
end
def ensure_listen_for_changes
return if @listen_for_changes == false
2013-02-05 14:16:51 -05:00
unless @subscribed
MessageBus.subscribe("/site_settings") do |message|
process_message(message) if message.data["process"] != process_id
2013-02-05 14:16:51 -05:00
end
2013-02-05 14:16:51 -05:00
@subscribed = true
end
end
2013-06-12 22:41:27 -04:00
def process_message(message)
begin
MessageBus.on_connect.call(message.site_id)
refresh!
ensure
MessageBus.on_disconnect.call(message.site_id)
2013-06-12 22:41:27 -04:00
end
end
2013-02-05 14:16:51 -05:00
def process_id
@process_id ||= SecureRandom.uuid
2013-02-05 14:16:51 -05:00
end
def after_fork
@process_id = nil
ensure_listen_for_changes
end
2013-02-05 14:16:51 -05:00
def remove_override!(name)
old_val = current[name]
provider.destroy(name)
current[name] = defaults.get(name, default_locale)
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
2013-02-05 14:16:51 -05:00
end
2015-03-02 12:12:19 -05:00
def add_override!(name, val)
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
provider.save(name, sanitized_val, type)
current[name] = type_supervisor.to_rb_value(name, sanitized_val)
return if current[name] == old_val
clear_uploads_cache(name)
2015-08-27 18:55:19 -04:00
notify_clients!(name) if client_settings.include? name
clear_cache!
if old_val != current[name]
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name])
end
end
def notify_changed!
MessageBus.publish("/site_settings", process: process_id)
2013-02-05 14:16:51 -05:00
end
2015-08-27 18:55:19 -04:00
def notify_clients!(name)
MessageBus.publish("/client_settings", name: name, value: self.public_send(name))
2015-08-27 18:55:19 -04:00
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
]
2014-07-24 08:00:15 -04:00
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
2014-07-24 08:00:15 -04:00
end
end
def set(name, value, options = nil)
if has_setting?(name)
2014-07-24 08:00:15 -04:00
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)
2014-01-27 13:05:35 -05:00
else
raise Discourse::InvalidParameters.new(
"Either no setting named '#{name}' exists or value provided is invalid",
)
2014-01-27 13:05:35 -05:00
end
end
def set_and_log(name, value, user = Discourse.system_user, detailed_message = nil)
if has_setting?(name)
prev_value = public_send(name)
set(name, value)
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,
)
else
raise Discourse::InvalidParameters.new(
I18n.t("errors.site_settings.invalid_site_setting", name: name),
)
end
end
def get(name)
if has_setting?(name)
self.public_send(name)
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
2013-02-25 11:42:20 -05:00
protected
2013-02-05 14:16:51 -05:00
def clear_cache!
Discourse.cache.delete(SiteSettingExtension.client_settings_cache_key)
Site.clear_anon_cache!
end
2013-06-12 22:41:27 -04:00
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
value
end
define_singleton_method "#{clean_name}?" do
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)
2015-02-11 23:07:17 -05:00
clean_name = name.to_s.sub("?", "").to_sym
2013-02-05 14:16:51 -05:00
if type_supervisor.get_type(name) == :uploaded_image_list
define_singleton_method clean_name do
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
2018-11-14 02:03:02 -05:00
define_singleton_method clean_name do
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
2018-11-14 02:03:02 -05:00
upload = Upload.find_by(id: value)
uploads[name] = upload if upload
end
end
else
define_singleton_method clean_name do
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 = current[name]
if mandatory_values[name]
return (mandatory_values[name].split("|") | value.to_s.split("|")).join("|")
2018-11-14 02:03:02 -05:00
end
value
end
end
2013-02-05 14:16:51 -05:00
# Any group_list setting, e.g. personal_message_enabled_groups, 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 type_supervisor.get_type(name) == :group_list
define_singleton_method("#{clean_name}_map") do
self.public_send(clean_name).to_s.split("|").map(&:to_i)
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
self.public_send(clean_name).to_s.split("|")
end
end
end
2015-02-11 23:07:17 -05:00
define_singleton_method "#{clean_name}?" do
self.public_send clean_name
2013-02-05 14:16:51 -05:00
end
2013-02-25 11:42:20 -05:00
2015-02-11 23:07:17 -05:00
define_singleton_method "#{clean_name}=" do |val|
add_override!(name, val)
2013-02-05 14:16:51 -05:00
end
end
2014-07-24 08:00:15 -04:00
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
2014-07-24 08:00:15 -04:00
end
private
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]
requires_confirmation_settings[name] = (
if SiteSettings::TypeSupervisor::REQUIRES_CONFIRMATION_TYPES.values.include?(
opts[:requires_confirmation],
)
opts[:requires_confirmation]
end
)
categories[name] = opts[:category] || :uncategorized
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]
type_supervisor.load_setting(
name,
opts.extract!(*SiteSettings::TypeSupervisor::CONSUMED_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
2018-11-14 02:03:02 -05:00
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
2013-02-05 14:16:51 -05:00
end