mirror of
https://github.com/discourse/discourse.git
synced 2025-01-19 11:02:46 +08:00
0e303c7f5d
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
535 lines
12 KiB
Ruby
535 lines
12 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_dependency 'site_settings/deprecated_settings'
|
|
require_dependency 'site_settings/type_supervisor'
|
|
require_dependency 'site_settings/defaults_provider'
|
|
require_dependency 'site_settings/db_provider'
|
|
|
|
module SiteSettingExtension
|
|
include SiteSettings::DeprecatedSettings
|
|
|
|
# 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?
|
|
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
|
|
|
|
def mutex
|
|
@mutex ||= Mutex.new
|
|
end
|
|
|
|
def current
|
|
@containers ||= {}
|
|
@containers[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 shadowed_settings
|
|
@shadowed_settings ||= []
|
|
end
|
|
|
|
def hidden_settings
|
|
@hidden_settings ||= []
|
|
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 ||= []
|
|
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)
|
|
)
|
|
|
|
categories[name] = opts[:category] || :uncategorized
|
|
|
|
if opts[:hidden]
|
|
hidden_settings << name
|
|
end
|
|
|
|
if opts[:shadowed_by_global] && GlobalSetting.respond_to?(name)
|
|
val = GlobalSetting.send(name)
|
|
|
|
unless val.nil? || (val == ''.freeze)
|
|
shadowed_val = val
|
|
hidden_settings << name
|
|
shadowed_settings << name
|
|
end
|
|
end
|
|
|
|
if opts[:refresh]
|
|
refresh_settings << name
|
|
end
|
|
|
|
if opts[:client]
|
|
client_settings << name.to_sym
|
|
end
|
|
|
|
if opts[:preview]
|
|
previews[name] = opts[:preview]
|
|
end
|
|
|
|
if opts[:secret]
|
|
secret_settings << name
|
|
end
|
|
|
|
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 settings_hash
|
|
result = {}
|
|
deprecated_settings = Set.new
|
|
|
|
SiteSettings::DeprecatedSettings::SETTINGS.each do |s|
|
|
deprecated_settings << s[0]
|
|
end
|
|
|
|
defaults.all.keys.each do |s|
|
|
result[s] =
|
|
if deprecated_settings.include?(s.to_s)
|
|
send(s, warn: false).to_s
|
|
else
|
|
send(s).to_s
|
|
end
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
def client_settings_json
|
|
Rails.cache.fetch(SiteSettingExtension.client_settings_cache_key, expires_in: 30.minutes) do
|
|
client_settings_json_uncached
|
|
end
|
|
end
|
|
|
|
def client_settings_json_uncached
|
|
MultiJson.dump(Hash[*@client_settings.map do |name|
|
|
value = self.public_send(name)
|
|
value = value.to_s if type_supervisor.get_type(name) == :upload
|
|
[name, value]
|
|
end.flatten])
|
|
end
|
|
|
|
# Retrieve all settings
|
|
def all_settings(include_hidden = false)
|
|
|
|
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?
|
|
}
|
|
|
|
defaults.all(default_locale)
|
|
.reject { |s, _| !include_hidden && hidden_settings.include?(s) }
|
|
.map do |s, v|
|
|
|
|
value = send(s)
|
|
type_hash = type_supervisor.type_hash(s)
|
|
default = defaults.get(s, default_locale).to_s
|
|
|
|
if type_hash[:type].to_s == "upload" &&
|
|
default.to_i < Upload::SEEDED_ID_THRESHOLD
|
|
|
|
default = default_uploads[default.to_i]
|
|
end
|
|
|
|
opts = {
|
|
setting: s,
|
|
description: description(s),
|
|
default: default,
|
|
value: value.to_s,
|
|
category: categories[s],
|
|
preview: previews[s],
|
|
secret: secret_settings.include?(s),
|
|
placeholder: placeholder(s)
|
|
}.merge!(type_hash)
|
|
|
|
opts
|
|
end.unshift(locale_setting_hash)
|
|
end
|
|
|
|
def description(setting)
|
|
I18n.t("site_settings.#{setting}", base_path: Discourse.base_path)
|
|
end
|
|
|
|
def placeholder(setting)
|
|
if !I18n.t("site_settings.placeholder.#{setting}", default: "").empty?
|
|
I18n.t("site_settings.placeholder.#{setting}")
|
|
elsif SiteIconManager.respond_to?("#{setting}_url")
|
|
SiteIconManager.public_send("#{setting}_url")
|
|
end
|
|
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
|
|
|
|
# refresh all the site settings
|
|
def refresh!
|
|
mutex.synchronize do
|
|
ensure_listen_for_changes
|
|
|
|
new_hash = Hash[*(defaults.db_all.map { |s|
|
|
[s.name.to_sym, type_supervisor.to_rb_value(s.name, s.value, s.data_type)]
|
|
}.to_a.flatten)]
|
|
|
|
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.send(ss) }
|
|
|
|
changes, deletions = diff_hash(new_hash, current)
|
|
|
|
changes.each do |name, val|
|
|
current[name] = val
|
|
clear_uploads_cache(name)
|
|
end
|
|
|
|
deletions.each do |name, _|
|
|
current[name] = defaults_view[name]
|
|
clear_uploads_cache(name)
|
|
end
|
|
|
|
clear_cache!
|
|
end
|
|
end
|
|
|
|
def ensure_listen_for_changes
|
|
return if @listen_for_changes == false
|
|
|
|
unless @subscribed
|
|
MessageBus.subscribe("/site_settings") do |message|
|
|
if message.data["process"] != process_id
|
|
process_message(message)
|
|
end
|
|
end
|
|
|
|
@subscribed = true
|
|
end
|
|
end
|
|
|
|
def process_message(message)
|
|
begin
|
|
@last_message_processed = message.global_id
|
|
MessageBus.on_connect.call(message.site_id)
|
|
refresh!
|
|
ensure
|
|
MessageBus.on_disconnect.call(message.site_id)
|
|
end
|
|
end
|
|
|
|
def diags
|
|
{
|
|
last_message_processed: @last_message_processed
|
|
}
|
|
end
|
|
|
|
def process_id
|
|
@process_id ||= SecureRandom.uuid
|
|
end
|
|
|
|
def after_fork
|
|
@process_id = nil
|
|
ensure_listen_for_changes
|
|
end
|
|
|
|
def remove_override!(name)
|
|
old_val = current[name]
|
|
provider.destroy(name)
|
|
current[name] = defaults.get(name, default_locale)
|
|
clear_uploads_cache(name)
|
|
clear_cache!
|
|
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name]) if old_val != current[name]
|
|
end
|
|
|
|
def add_override!(name, val)
|
|
old_val = current[name]
|
|
val, type = type_supervisor.to_db_value(name, val)
|
|
provider.save(name, val, type)
|
|
current[name] = type_supervisor.to_rb_value(name, val)
|
|
clear_uploads_cache(name)
|
|
notify_clients!(name) if client_settings.include? name
|
|
clear_cache!
|
|
DiscourseEvent.trigger(:site_setting_changed, name, old_val, current[name]) if old_val != current[name]
|
|
end
|
|
|
|
def notify_changed!
|
|
MessageBus.publish('/site_settings', process: process_id)
|
|
end
|
|
|
|
def notify_clients!(name)
|
|
MessageBus.publish('/client_settings', name: name, value: self.send(name))
|
|
end
|
|
|
|
def requires_refresh?(name)
|
|
refresh_settings.include?(name.to_sym)
|
|
end
|
|
|
|
HOSTNAME_SETTINGS ||= %w{
|
|
disabled_image_download_domains onebox_domains_blacklist exclude_rel_nofollow_domains
|
|
email_domains_blacklist email_domains_whitelist white_listed_spam_host_domains
|
|
}
|
|
|
|
def filter_value(name, value)
|
|
if HOSTNAME_SETTINGS.include?(name)
|
|
value.split("|").map { |url| url.strip!; get_hostname(url) }.compact.uniq.join("|")
|
|
else
|
|
value
|
|
end
|
|
end
|
|
|
|
def set(name, value)
|
|
if has_setting?(name)
|
|
value = filter_value(name, value)
|
|
self.send("#{name}=", value)
|
|
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)
|
|
prev_value = send(name)
|
|
set(name, value)
|
|
if has_setting?(name)
|
|
value = prev_value = "[FILTERED]" if secret_settings.include?(name.to_sym)
|
|
StaffActionLogger.new(user).log_site_setting_change(name, prev_value, value)
|
|
end
|
|
end
|
|
|
|
protected
|
|
|
|
def clear_cache!
|
|
Rails.cache.delete(SiteSettingExtension.client_settings_cache_key)
|
|
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 do |name, value|
|
|
deletions << [name, value] unless new_hash.has_key?(name)
|
|
end
|
|
|
|
[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|
|
|
Rails.logger.warn("An attempt was to change #{clean_name} SiteSetting to #{val} however it is shadowed so this will be ignored!")
|
|
nil
|
|
end
|
|
|
|
end
|
|
|
|
def setup_methods(name)
|
|
clean_name = name.to_s.sub("?", "").to_sym
|
|
|
|
if type_supervisor.get_type(name) == :upload
|
|
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
|
|
upload = Upload.find_by(id: value)
|
|
uploads[name] = upload if upload
|
|
end
|
|
end
|
|
else
|
|
define_singleton_method clean_name do
|
|
if (c = current[name]).nil?
|
|
refresh!
|
|
current[name]
|
|
else
|
|
c
|
|
end
|
|
end
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}?" do
|
|
self.send clean_name
|
|
end
|
|
|
|
define_singleton_method "#{clean_name}=" do |val|
|
|
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
|
|
|
|
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 && uploads.has_key?(name)
|
|
uploads.delete(name)
|
|
end
|
|
end
|
|
|
|
def logger
|
|
Rails.logger
|
|
end
|
|
|
|
end
|