diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index d796e274921..73e8160a28c 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -64,16 +64,16 @@ export default Ember.Component.extend(BufferedContent, { }.on("willDestroyElement"), _save() { - const self = this, - setting = this.get('buffered'); - SiteSetting.update(setting.get('setting'), setting.get('value')).then(function() { - self.set('validationMessage', null); - self.commitBuffer(); - }).catch(function(e) { + const setting = this.get('buffered'), + action = SiteSetting.update(setting.get('setting'), setting.get('value')); + action.then(() => { + this.set('validationMessage', null); + this.commitBuffer(); + }).catch((e) => { if (e.jqXHR.responseJSON && e.jqXHR.responseJSON.errors) { - self.set('validationMessage', e.jqXHR.responseJSON.errors[0]); + this.set('validationMessage', e.jqXHR.responseJSON.errors[0]); } else { - self.set('validationMessage', I18n.t('generic_error')); + this.set('validationMessage', I18n.t('generic_error')); } }); }, diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js.es6 index 51037626634..6ed78787968 100644 --- a/app/assets/javascripts/admin/models/site-setting.js.es6 +++ b/app/assets/javascripts/admin/models/site-setting.js.es6 @@ -48,7 +48,7 @@ SiteSetting.reopenClass({ update(key, value) { const data = {}; data[key] = value; - return ajax("/admin/site_settings/" + key, { type: 'PUT', data }); + return ajax(`/admin/site_settings/${key}`, { type: 'PUT', data }); } }); diff --git a/app/controllers/admin/site_settings_controller.rb b/app/controllers/admin/site_settings_controller.rb index 87478597a17..ed64aa2d4bb 100644 --- a/app/controllers/admin/site_settings_controller.rb +++ b/app/controllers/admin/site_settings_controller.rb @@ -1,4 +1,7 @@ class Admin::SiteSettingsController < Admin::AdminController + rescue_from Discourse::InvalidParameters do |e| + render_json_error e.message, status: 422 + end def index render_json_dump(site_settings: SiteSetting.all_settings, diags: SiteSetting.diags) @@ -9,15 +12,17 @@ class Admin::SiteSettingsController < Admin::AdminController id = params[:id] value = params[id] value.strip! if value.is_a?(String) - begin - # note, as of Ruby 2.3 symbols are GC'd so this is considered safe - if SiteSetting.hidden_settings.include?(id.to_sym) - raise Discourse::InvalidParameters, "You are not allowed to change hidden settings" - end - SiteSetting.set_and_log(id, value, current_user) - render nothing: true - rescue Discourse::InvalidParameters => e - render json: { errors: [e.message] }, status: 422 + raise_access_hidden_setting(id) + SiteSetting.set_and_log(id, value, current_user) + render nothing: true + end + + private + + def raise_access_hidden_setting(id) + # note, as of Ruby 2.3 symbols are GC'd so this is considered safe + if SiteSetting.hidden_settings.include?(id.to_sym) + raise Discourse::InvalidParameters, "You are not allowed to change hidden settings" end end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 2e4fa838f34..4159a1cfb5b 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -14,11 +14,7 @@ class SiteSetting < ActiveRecord::Base def self.load_settings(file) SiteSettings::YamlLoader.new(file).load do |category, name, default, opts| - if opts.delete(:client) - client_setting(name, default, opts.merge(category: category)) - else - setting(name, default, opts.merge(category: category)) - end + setting(name, default, opts.merge(category: category)) end end @@ -31,6 +27,11 @@ class SiteSetting < ActiveRecord::Base end end + # `current` hash is not populated everytime when load a site setting + # in order to support locale default. Instead, we simply `refresh!` once. + # This should only affects the spec in which you should populate `current` + refresh! + client_settings << :available_locales def self.available_locales diff --git a/config/environments/development.rb b/config/environments/development.rb index d9c10db2e8a..dc0a0cda2e7 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -59,6 +59,9 @@ Discourse::Application.configure do end config.after_initialize do + SiteSetting.defaults.set_regardless_of_locale(:port, 3000) + SiteSetting.refresh! + if ENV['BULLET'] Bullet.enable = true Bullet.rails_logger = true diff --git a/config/environments/test.rb b/config/environments/test.rb index 5cd785ce22c..c2129f70953 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -46,4 +46,22 @@ Discourse::Application.configure do config.logger = Logger.new(nil) config.log_level = :fatal end + + config.after_initialize do + SiteSetting.defaults.tap do |s| + s.set_regardless_of_locale(:s3_upload_bucket, 'bucket') + s.set_regardless_of_locale(:min_post_length, 5) + s.set_regardless_of_locale(:min_first_post_length, 5) + s.set_regardless_of_locale(:min_private_message_post_length, 10) + s.set_regardless_of_locale(:crawl_images, false) + s.set_regardless_of_locale(:download_remote_images_to_local, false) + s.set_regardless_of_locale(:unique_posts_mins, 0) + s.set_regardless_of_locale(:queue_jobs, false) + # disable plugins + if ENV['LOAD_PLUGINS'] == '1' + s.set_regardless_of_locale(:discourse_narrative_bot_enabled, false) + end + end + SiteSetting.refresh! + end end diff --git a/config/site_settings.yml b/config/site_settings.yml index b9092e186cf..3f85d1e1aa1 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -12,6 +12,9 @@ # shadowed_by_global - "Shadow" a site setting with a GlobalSetting. If the GlobalSetting # exists it will be used instead of the setting and the setting will be hidden. # Useful for things like API keys on multisite. +# locale_default - A hash which overrides according to `SiteSetting.default_locale`. +# The key should be as the same as possible value of default_locale. +# # # type: email - Must be a valid email address. # type: username - Must match the username of an existing user. @@ -68,11 +71,6 @@ required: default: '' basic: - default_locale: - default: 'en' - enum: 'LocaleSiteSetting' - refresh: true - shadowed_by_global: true allow_user_locale: client: true default: false @@ -432,21 +430,24 @@ posting: min_post_length: client: true min: 1 - default: - test: 5 - default: 20 + default: 20 + locale_default: + zh_CN: 8 + zh_TW: 8 min_first_post_length: client: true min: 1 - default: - test: 5 - default: 20 + default: 20 + locale_default: + zh_CN: 8 + zh_TW: 8 min_private_message_post_length: client: true min: 1 - default: - test: 5 - default: 10 + default: 10 + locale_default: + zh_CN: 3 + zh_TW: 3 max_post_length: client: true default: 32000 @@ -454,17 +455,32 @@ posting: topic_featured_link_enabled: client: true default: true - body_min_entropy: 7 + body_min_entropy: + default: 7 + locale_default: + zh_CN: 3 + zh_TW: 3 min_topic_title_length: client: true default: 15 + locale_default: + zh_CN: 6 + zh_TW: 6 max_topic_title_length: client: true default: 255 max: 255 - title_min_entropy: 10 + title_min_entropy: + default: 10 + locale_default: + zh_CN: 3 + zh_TW: 3 allow_uppercase_posts: false - title_prettify: true + title_prettify: + default: true + locale_default: + zh_CN: false + zh_TW: false title_fancy_entities: true min_private_message_title_length: client: true @@ -478,9 +494,15 @@ posting: min_title_similar_length: client: true default: 10 + locale_default: + zh_CN: 4 + zh_TW: 4 min_body_similar_length: client: true default: 15 + locale_default: + zh_CN: 5 + zh_TW: 5 enable_private_messages: default: true client: true @@ -524,7 +546,11 @@ posting: newuser_max_attachments: client: true default: 0 - post_excerpt_maxlength: 300 + post_excerpt_maxlength: + default: 300 + locale_default: + zh_CN: 120 + zh_TW: 120 show_pinned_excerpt_mobile: client: true default: true @@ -603,7 +629,11 @@ email: client: true private_email_time_window_seconds: 20 email_posts_context: 5 - digest_min_excerpt_length: 100 + digest_min_excerpt_length: + default: 100 + locale_default: + zh_CN: 50 + zh_TW: 50 digest_topics: default: 5 min: 1 @@ -726,9 +756,7 @@ files: refresh: true type: list crawl_images: - default: - test: false - default: true + default: true max_image_width: client: true default: 690 @@ -736,9 +764,7 @@ files: client: true default: 500 download_remote_images_to_local: - default: - test: false - default: true + default: true download_remote_images_threshold: 10 download_remote_images_max_days_old: default: 30 @@ -908,7 +934,11 @@ security: onebox: enable_flash_video_onebox: false - post_onebox_maxlength: 500 + post_onebox_maxlength: + default: 500 + locale_default: + zh_CN: 200 + zh_TW: 200 onebox_domains_blacklist: default: '' type: list @@ -954,10 +984,7 @@ spam: auto_block_first_post_regex: "" rate_limits: - unique_posts_mins: - default: - test: 0 - default: 5 + unique_posts_mins: 5 rate_limit_create_topic: 15 rate_limit_create_post: 5 rate_limit_new_user_create_topic: 120 @@ -995,28 +1022,20 @@ rate_limits: developer: force_hostname: - hidden: - development: false - default: true + hidden: true default: '' port: - hidden: - development: false - default: true - default: - development: 3000 - default: '' + hidden: true + default: '' queue_jobs: - hidden: - development: false - default: true - default: - test: false - default: true + hidden: true + default: true enable_long_polling: client: true default: true - long_polling_interval: 25000 + long_polling_interval: + default: 25000 + max: 25000 long_polling_base_url: client: true default: '/' @@ -1144,12 +1163,19 @@ search: min_search_term_length: client: true default: 3 + locale_default: + zh_CN: 2 + zh_TW: 2 search_tokenize_chinese_japanese_korean: false search_prefer_recent_posts: false - search_recent_posts_size: 100000 + search_recent_posts_size: + default: 100000 + max: 100000 log_search_queries: true - search_query_log_max_size: 1000000 + search_query_log_max_size: + default: 1000000 + max: 1000000 uncategorized: version_checks: @@ -1166,6 +1192,9 @@ uncategorized: slug_generation_method: default: 'ascii' enum: 'SlugSetting' + locale_default: + zh_CN: 'none' + zh_TW: 'none' permalink_normalizations: default: '' @@ -1306,6 +1335,9 @@ uncategorized: read_time_word_count: default: 500 client: true + locale_default: + zh_CN: 350 + zh_TW: 350 topic_page_title_includes_category: true @@ -1323,7 +1355,7 @@ user_preferences: default_email_mailing_list_mode: false default_email_mailing_list_mode_frequency: enum: 'MailingListModeSiteSetting' - default: 0 + default: 1 disable_mailing_list_mode: default: false client: true diff --git a/db/migrate/20170725075535_correct_default_email_mailing_list_mode_frequency.rb b/db/migrate/20170725075535_correct_default_email_mailing_list_mode_frequency.rb new file mode 100644 index 00000000000..d2e4cdd0688 --- /dev/null +++ b/db/migrate/20170725075535_correct_default_email_mailing_list_mode_frequency.rb @@ -0,0 +1,8 @@ +class CorrectDefaultEmailMailingListModeFrequency < ActiveRecord::Migration + def up + execute "UPDATE site_settings SET value = '1' WHERE value = '0' AND name = 'default_email_mailing_list_mode_frequency';" + end + + def down + end +end diff --git a/lib/discourse.rb b/lib/discourse.rb index dd9d9d3fef9..cd6a27e4401 100644 --- a/lib/discourse.rb +++ b/lib/discourse.rb @@ -84,6 +84,8 @@ module Discourse # Cross site request forgery class CSRF < StandardError; end + class Deprecation < StandardError; end + def self.filters @filters ||= [:latest, :unread, :new, :read, :posted, :bookmarks] end diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 5b9fb8987d6..410f22bc17c 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -1,14 +1,16 @@ -require_dependency 'enum' +require_dependency 'site_settings/deprecated_settings' +require_dependency 'site_settings/type_supervisor' +require_dependency 'site_settings/defaults_provider' require_dependency 'site_settings/db_provider' -require 'site_setting_validations' module SiteSettingExtension - include SiteSettingValidations + include SiteSettings::DeprecatedSettings + extend Forwardable - # For plugins, so they can tell if a feature is supported - def supported_types - [:email, :username, :list, :enum] - end + def_delegator :defaults, :site_locale, :default_locale + def_delegator :defaults, :site_locale=, :default_locale= + def_delegator :defaults, :has_setting? + def_delegators 'SiteSettings::TypeSupervisor', :types, :supported_types # part 1 of refactor, centralizing the dependency here def provider=(val) @@ -20,22 +22,6 @@ module SiteSettingExtension @provider ||= SiteSettings::DbProvider.new(SiteSetting) end - def types - @types ||= Enum.new(string: 1, - time: 2, - integer: 3, - float: 4, - bool: 5, - null: 6, - enum: 7, - list: 8, - url_list: 9, - host_list: 10, - category_list: 11, - value_list: 12, - regex: 13) - end - def mutex @mutex ||= Mutex.new end @@ -46,25 +32,17 @@ module SiteSettingExtension end def defaults - @defaults ||= {} + @defaults ||= SiteSettings::DefaultsProvider.new(self) + end + + def type_supervisor + @type_supervisor ||= SiteSettings::TypeSupervisor.new(defaults) end def categories @categories ||= {} end - def enums - @enums ||= {} - end - - def static_types - @static_types ||= {} - end - - def choices - @choices ||= {} - end - def shadowed_settings @shadowed_settings ||= [] end @@ -85,34 +63,14 @@ module SiteSettingExtension @previews ||= {} end - def validators - @validators ||= {} - end - def setting(name_arg, default = nil, opts = {}) name = name_arg.to_sym mutex.synchronize do - self.defaults[name] = default + defaults.load_setting(name, + default, + opts.extract!(*SiteSettings::DefaultsProvider::CONSUMED_OPTS)) + categories[name] = opts[:category] || :uncategorized - current_value = current.has_key?(name) ? current[name] : default - - if enum = opts[:enum] - enums[name] = enum.is_a?(String) ? enum.constantize : enum - opts[:type] ||= :enum - end - - if new_choices = opts[:choices] - - new_choices = eval(new_choices) if new_choices.is_a?(String) - - choices.has_key?(name) ? - choices[name].concat(new_choices) : - choices[name] = new_choices - end - - if type = opts[:type] - static_types[name.to_sym] = type.to_sym - end if opts[:hidden] hidden_settings << name @@ -124,7 +82,6 @@ module SiteSettingExtension unless val.nil? || (val == ''.freeze) hidden_settings << name shadowed_settings << name - current_value = val end end @@ -132,31 +89,24 @@ module SiteSettingExtension refresh_settings << name end + if opts[:client] + client_settings << name.to_sym + end + if opts[:preview] previews[name] = opts[:preview] end - opts[:validator] = opts[:validator].try(:constantize) - type = opts[:type] || get_data_type(name, defaults[name]) + type_supervisor.load_setting(name, + opts.extract!(*SiteSettings::TypeSupervisor::CONSUMED_OPTS)) - if validator_type = opts[:validator] || validator_for(type) - validators[name] = { class: validator_type, opts: opts } - end - - current[name] = current_value setup_methods(name) end end - # just like a setting, except that it is available in javascript via DiscourseSession - def client_setting(name, default = nil, opts = {}) - setting(name, default, opts) - client_settings << name.to_sym - end - def settings_hash result = {} - @defaults.each do |s, _| + defaults.each_key do |s| result[s] = send(s).to_s end result @@ -174,32 +124,21 @@ module SiteSettingExtension # Retrieve all settings def all_settings(include_hidden = false) - @defaults - .reject { |s, _| hidden_settings.include?(s) && !include_hidden } + defaults + .reject { |s, _| !include_hidden && hidden_settings.include?(s) } .map do |s, v| - value = send(s) - type = types[get_data_type(s, value)] - opts = { - setting: s, - description: description(s), - default: v.to_s, - type: type.to_s, - value: value.to_s, - category: categories[s], - preview: previews[s] - } + value = send(s) + opts = { + setting: s, + description: description(s), + default: defaults[s].to_s, + value: value.to_s, + category: categories[s], + preview: previews[s] + }.merge(type_supervisor.type_hash(s)) - if type == :enum && enum_class(s) - opts.merge!(valid_values: enum_class(s).values, translate_names: enum_class(s).translate_names?) - elsif type == :enum - opts.merge!(valid_values: choices[s].map { |c| { name: c, value: c } }, translate_names: false) - end - - opts[:textarea] = true if static_types[s] == :textarea - - opts[:choices] = choices[s] if choices.has_key? s - opts - end + opts + end.unshift(defaults.locale_setting_hash) end def description(setting) @@ -217,22 +156,22 @@ module SiteSettingExtension def refresh! mutex.synchronize do ensure_listen_for_changes - old = current - new_hash = Hash[*(provider.all.map { |s| - [s.name.intern, convert(s.value, s.data_type, s.name)] + 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)] - # add defaults, cause they are cached - new_hash = defaults.merge(new_hash) + defaults_view = defaults.all + + # 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, old) - + changes, deletions = diff_hash(new_hash, current) changes.each { |name, val| current[name] = val } - deletions.each { |name, val| current[name] = defaults[name] } + deletions.each { |name, _| current[name] = defaults_view[name] } clear_cache! end @@ -282,44 +221,9 @@ module SiteSettingExtension end def add_override!(name, val) - type = get_data_type(name, defaults[name.to_sym]) - - val = val.to_s if type == types[:string] - - if type == types[:bool] && val != true && val != false - val = (val == "t" || val == "true") ? 't' : 'f' - end - - if type == types[:integer] && !val.is_a?(Integer) - val = val.to_i - end - - if type == types[:null] && val != '' - type = get_data_type(name, val) - end - - if type == types[:enum] - val = val.to_i if defaults[name.to_sym].is_a?(Integer) - if enum_class(name) - raise Discourse::InvalidParameters.new(:value) unless enum_class(name).valid_value?(val) - else - raise Discourse::InvalidParameters.new(:value) unless choices[name].include?(val) - end - end - - if v = validators[name] - validator = v[:class].new(v[:opts]) - unless validator.valid_value?(val) - raise Discourse::InvalidParameters.new(validator.error_message) - end - end - - if self.respond_to? "validate_#{name}" - send("validate_#{name}", val) - end - + val, type = type_supervisor.to_db_value(name, val) provider.save(name, val, type) - current[name] = convert(val, type, name) + current[name] = type_supervisor.to_rb_value(name, val) notify_clients!(name) if client_settings.include? name clear_cache! end @@ -332,26 +236,10 @@ module SiteSettingExtension MessageBus.publish('/client_settings', name: name, value: self.send(name)) end - def has_setting?(name) - defaults.has_key?(name.to_sym) || defaults.has_key?("#{name}?".to_sym) - end - def requires_refresh?(name) refresh_settings.include?(name.to_sym) end - def is_valid_data?(name, value) - valid = true - type = get_data_type(name, defaults[name.to_sym]) - - if type == types[:integer] - # validate integer - valid = false unless value.to_i.is_a?(Integer) - end - - valid - end - def filter_value(name, value) if %w[disabled_image_download_domains onebox_domains_blacklist exclude_rel_nofollow_domains email_domains_blacklist email_domains_whitelist white_listed_spam_host_domains].include? name domain_array = [] @@ -362,7 +250,7 @@ module SiteSettingExtension end def set(name, value) - if has_setting?(name) && is_valid_data?(name, value) + if has_setting?(name) value = filter_value(name, value) self.send("#{name}=", value) Discourse.request_refresh! if requires_refresh?(name) @@ -399,90 +287,11 @@ module SiteSettingExtension [changes, deletions] end - def get_data_type(name, val) - return types[:null] if val.nil? - - # Some types are just for validations like email. - # Only consider it valid if includes in `types` - if static_type = static_types[name.to_sym] - return types[static_type] if types.keys.include?(static_type) - end - - case val - when String - types[:string] - when Integer - types[:integer] - when Float - types[:float] - when TrueClass, FalseClass - types[:bool] - else - raise ArgumentError.new :val - end - end - - def convert(value, type, name) - case type - when types[:float] - value.to_f - when types[:integer] - value.to_i - when types[:bool] - value == true || value == "t" || value == "true" - when types[:null] - nil - when types[:enum] - defaults[name.to_sym].is_a?(Integer) ? value.to_i : value - else - return value if types[type] - # Otherwise it's a type error - raise ArgumentError.new :type - end - end - - def validator_for(type_name) - @validator_mapping ||= { - 'email' => EmailSettingValidator, - 'username' => UsernameSettingValidator, - types[:integer] => IntegerSettingValidator, - types[:string] => StringSettingValidator, - 'list' => StringSettingValidator, - 'enum' => StringSettingValidator, - 'regex' => RegexSettingValidator - } - @validator_mapping[type_name] - end - - DEPRECATED_SETTINGS = [ - ['use_https', 'force_https', '1.7'] - ] - - def setup_deprecated_methods - DEPRECATED_SETTINGS.each do |old_setting, new_setting, version| - define_singleton_method old_setting do - logger.warn("`SiteSetting.#{old_setting}` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}` instead") - self.public_send new_setting - end - - define_singleton_method "#{old_setting}?" do - logger.warn("`SiteSetting.#{old_setting}?` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}?` instead") - self.public_send "#{new_setting}?" - end - - define_singleton_method "#{old_setting}=" do |val| - logger.warn("`SiteSetting.#{old_setting}=` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}=` instead") - self.public_send "#{new_setting}=", val - end - end - end - def setup_methods(name) clean_name = name.to_s.sub("?", "").to_sym define_singleton_method clean_name do - c = @containers[provider.current_site] - if c + if (c = @containers[provider.current_site]) c[name] else refresh! @@ -499,10 +308,6 @@ module SiteSettingExtension end end - def enum_class(name) - enums[name] - end - def get_hostname(url) unless (URI.parse(url).scheme rescue nil).nil? url = "http://#{url}" if URI.parse(url).scheme.nil? diff --git a/lib/site_settings/defaults_provider.rb b/lib/site_settings/defaults_provider.rb new file mode 100644 index 00000000000..4b7b432912b --- /dev/null +++ b/lib/site_settings/defaults_provider.rb @@ -0,0 +1,126 @@ +module SiteSettings; end + +# A cache for providing default value based on site locale +class SiteSettings::DefaultsProvider + include Enumerable + + CONSUMED_OPTS = %i[default locale_default].freeze + DEFAULT_LOCALE_KEY = :default_locale + DEFAULT_LOCALE = 'en'.freeze + DEFAULT_CATEGORY = 'required'.freeze + + def initialize(site_setting) + @site_setting = site_setting + @site_setting.refresh_settings << DEFAULT_LOCALE_KEY + + @cached = {} + @defaults = {} + @defaults[DEFAULT_LOCALE.to_sym] = {} + @site_locale = nil + refresh_site_locale! + end + + def load_setting(name_arg, value, opts = {}) + name = name_arg.to_sym + @defaults[DEFAULT_LOCALE.to_sym][name] = value + + if (locale_default = opts[:locale_default]) + locale_default.each do |locale, v| + locale = locale.to_sym + @defaults[locale] ||= {} + @defaults[locale][name] = v + end + end + refresh_cache! + end + + def db_all + @site_setting.provider.all.delete_if { |s| s.name.to_sym == DEFAULT_LOCALE_KEY } + end + + def all + @cached + end + + def get(name) + @cached[name.to_sym] + end + + # Used to override site settings in dev/test env + def set_regardless_of_locale(name, value) + name = name.to_sym + if @site_setting.has_setting?(name) + @defaults.each { |_, hash| hash.delete(name) } + @defaults[DEFAULT_LOCALE.to_sym][name] = value + value, type = @site_setting.type_supervisor.to_db_value(name, value) + @cached[name] = @site_setting.type_supervisor.to_rb_value(name, value, type) + else + raise ArgumentError.new("No setting named '#{name}' exists") + end + end + + alias [] get + + attr_reader :site_locale + + def site_locale=(val) + val = val.to_s + raise Discourse::InvalidParameters.new(:value) unless LocaleSiteSetting.valid_value?(val) + + if val != @site_locale + @site_setting.provider.save(DEFAULT_LOCALE_KEY, val, SiteSetting.types[:string]) + refresh_site_locale! + @site_setting.refresh! + Discourse.request_refresh! + end + + @site_locale + end + + def each + @cached.each { |k, v| yield k.to_sym, v } + end + + def locale_setting_hash + { + setting: DEFAULT_LOCALE_KEY, + default: DEFAULT_LOCALE, + category: DEFAULT_CATEGORY, + description: @site_setting.description(DEFAULT_LOCALE_KEY), + type: SiteSetting.types[SiteSetting.types[:enum]], + preview: nil, + value: @site_locale, + valid_values: LocaleSiteSetting.values, + translate_names: LocaleSiteSetting.translate_names? + } + end + + def refresh_site_locale! + if GlobalSetting.respond_to?(DEFAULT_LOCALE_KEY) && + (global_val = GlobalSetting.send(DEFAULT_LOCALE_KEY)) && + !global_val.blank? + @site_locale = global_val + elsif (db_val = @site_setting.provider.find(DEFAULT_LOCALE_KEY)) + @site_locale = db_val.value.to_s + else + @site_locale = DEFAULT_LOCALE + end + refresh_cache! + @site_locale + end + + def has_setting?(name) + has_key?(name.to_sym) || has_key?("#{name.to_s}?".to_sym) + end + + private + + def has_key?(key) + @cached.key?(key) || key == DEFAULT_LOCALE_KEY + end + + def refresh_cache! + @cached = @defaults[DEFAULT_LOCALE.to_sym].merge(@defaults.fetch(@site_locale.to_sym, {})) + end + +end diff --git a/lib/site_settings/deprecated_settings.rb b/lib/site_settings/deprecated_settings.rb new file mode 100644 index 00000000000..ca63777b4c3 --- /dev/null +++ b/lib/site_settings/deprecated_settings.rb @@ -0,0 +1,26 @@ +module SiteSettings; end + +module SiteSettings::DeprecatedSettings + DEPRECATED_SETTINGS = [ + %w[use_https force_https 1.7] + ] + + def setup_deprecated_methods + DEPRECATED_SETTINGS.each do |old_setting, new_setting, version| + define_singleton_method old_setting do + logger.warn("`SiteSetting.#{old_setting}` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}` instead") + self.public_send new_setting + end + + define_singleton_method "#{old_setting}?" do + logger.warn("`SiteSetting.#{old_setting}?` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}?` instead") + self.public_send "#{new_setting}?" + end + + define_singleton_method "#{old_setting}=" do |val| + logger.warn("`SiteSetting.#{old_setting}=` has been deprecated and will be removed in the #{version} Release. Please use `SiteSetting.#{new_setting}=` instead") + self.public_send "#{new_setting}=", val + end + end + end +end diff --git a/lib/site_settings/local_process_provider.rb b/lib/site_settings/local_process_provider.rb index a9451505459..e3974b0ea4d 100644 --- a/lib/site_settings/local_process_provider.rb +++ b/lib/site_settings/local_process_provider.rb @@ -10,7 +10,7 @@ class SiteSettings::LocalProcessProvider @settings[current_site] ||= {} end - def initialize() + def initialize @settings = {} self.current_site = "test" end diff --git a/lib/site_settings/type_supervisor.rb b/lib/site_settings/type_supervisor.rb new file mode 100644 index 00000000000..0cd9f272bc9 --- /dev/null +++ b/lib/site_settings/type_supervisor.rb @@ -0,0 +1,210 @@ +require_dependency 'site_settings/validations' +require_dependency 'enum' + +module SiteSettings; end + +class SiteSettings::TypeSupervisor + include SiteSettings::Validations + + CONSUMED_OPTS = %i[enum choices type validator min max regex hidden regex_error].freeze + VALIDATOR_OPTS = %i[min max regex hidden regex_error].freeze + + # For plugins, so they can tell if a feature is supported + SUPPORTED_TYPES = %i[email username list enum].freeze + + def self.types + @types ||= Enum.new(string: 1, + time: 2, + integer: 3, + float: 4, + bool: 5, + null: 6, + enum: 7, + list: 8, + url_list: 9, + host_list: 10, + category_list: 11, + value_list: 12, + regex: 13, + email: 14, + username: 15) + end + + def self.parse_value_type(val) + case val + when NilClass + self.types[:null] + when String + self.types[:string] + when Integer + self.types[:integer] + when Float + self.types[:float] + when TrueClass, FalseClass + self.types[:bool] + else + raise ArgumentError.new :val + end + end + + def self.supported_types + SUPPORTED_TYPES + end + + def initialize(defaults_provider) + @defaults_provider = defaults_provider + @enums = {} + @static_types = {} + @choices = {} + @validators = {} + @types = {} + end + + def load_setting(name_arg, opts = {}) + name = name_arg.to_sym + + if (enum = opts[:enum]) + @enums[name] = enum.is_a?(String) ? enum.constantize : enum + opts[:type] ||= :enum + end + + if (new_choices = opts[:choices]) + new_choices = eval(new_choices) if new_choices.is_a?(String) + + if @choices.has_key?(name) + @choices[name].concat(new_choices) + else + @choices[name] = new_choices + end + end + + if (type = opts[:type]) + @static_types[name] = type.to_sym + end + @types[name] = get_data_type(name, @defaults_provider[name]) + + opts[:validator] = opts[:validator].try(:constantize) + if (validator_type = (opts[:validator] || validator_for(@types[name]))) + @validators[name] = { class: validator_type, opts: opts.slice(*VALIDATOR_OPTS) } + end + end + + def to_rb_value(name, value, override_type = nil) + name = name.to_sym + type = @types[name] = (override_type || @types[name] || get_data_type(name, value)) + + case type + when self.class.types[:float] + value.to_f + when self.class.types[:integer] + value.to_i + when self.class.types[:bool] + value == true || value == 't' || value == 'true' + when self.class.types[:null] + nil + when self.class.types[:enum] + @defaults_provider[name].is_a?(Integer) ? value.to_i : value.to_s + when self.class.types[:string] + value.to_s + else + return value if self.class.types[type] + # Otherwise it's a type error + raise ArgumentError.new :type + end + end + + def to_db_value(name, value) + val, type = normalize_input(name, value) + validate_value(name, type, val) + [val, type] + end + + def type_hash(name) + name = name.to_sym + type = self.class.types[@types[name]] + + result = { type: type.to_s } + + if type == :enum + if (klass = enum_class(name)) + result.merge!(valid_values: klass.values, translate_names: klass.translate_names?) + else + result.merge!(valid_values: @choices[name].map { |c| { name: c, value: c } }, translate_names: false) + end + end + + result[:choices] = @choices[name] if @choices.has_key? name + result + end + + private + + def normalize_input(name, val) + name = name.to_sym + type = @types[name] || self.class.parse_value_type(val) + + if type == self.class.types[:bool] + val = (val == true || val == 't' || val == 'true') ? 't' : 'f' + elsif type == self.class.types[:integer] && !val.is_a?(Integer) + val = val.to_i + elsif type == self.class.types[:null] && val != '' + type = get_data_type(name, val) + elsif type == self.class.types[:enum] + val = @defaults_provider[name].is_a?(Integer) ? val.to_i : val.to_s + end + + [val, type] + end + + def validate_value(name, type, val) + if type == self.class.types[:enum] + if enum_class(name) + raise Discourse::InvalidParameters.new(:value) unless enum_class(name).valid_value?(val) + else + raise Discourse::InvalidParameters.new(:value) unless @choices[name].include?(val) + end + end + + if (v = @validators[name]) + validator = v[:class].new(v[:opts]) + unless validator.valid_value?(val) + raise Discourse::InvalidParameters, "#{name.to_s}: #{validator.error_message}" + end + end + + validate_method = "validate_#{name}" + if self.respond_to? validate_method + send(validate_method, val) + end + end + + def get_data_type(name, val) + # Some types are just for validations like email. + # Only consider it valid if includes in `types` + if (static_type = @static_types[name.to_sym]) + return self.class.types[static_type] if self.class.types.keys.include?(static_type) + end + + self.class.parse_value_type(val) + end + + def enum_class(name) + @enums[name] + end + + def validator_for(type_name) + case type_name + when self.class.types[:email] + EmailSettingValidator + when self.class.types[:username] + UsernameSettingValidator + when self.class.types[:integer] + IntegerSettingValidator + when self.class.types[:regex] + RegexSettingValidator + when self.class.types[:string], self.class.types[:list], self.class.types[:enum] + StringSettingValidator + else nil + end + end +end diff --git a/lib/site_setting_validations.rb b/lib/site_settings/validations.rb similarity index 97% rename from lib/site_setting_validations.rb rename to lib/site_settings/validations.rb index 7858c68f0e8..0643ab49b6a 100644 --- a/lib/site_setting_validations.rb +++ b/lib/site_settings/validations.rb @@ -1,6 +1,6 @@ +module SiteSettings; end -module SiteSettingValidations - +module SiteSettings::Validations def validate_error(key) raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}")) end diff --git a/lib/site_settings/yaml_loader.rb b/lib/site_settings/yaml_loader.rb index 5142f5aab1a..787be9848e2 100644 --- a/lib/site_settings/yaml_loader.rb +++ b/lib/site_settings/yaml_loader.rb @@ -1,32 +1,26 @@ module SiteSettings; end class SiteSettings::YamlLoader - def initialize(file) @file = file end - def env_val(value) - if value.is_a?(Hash) - value.has_key?(Rails.env) ? value[Rails.env] : value['default'] - else - value - end - end - def load yaml = YAML.load_file(@file) yaml.each_key do |category| yaml[category].each do |setting_name, hash| if hash.is_a?(Hash) # Get default value for the site setting: - value = env_val(hash.delete('default')) - - if hash.key?('hidden') - hash['hidden'] = env_val(hash.delete('hidden')) + value = hash.delete('default') + if value.is_a?(Hash) + raise Discourse::Deprecation, "Site setting per env is no longer supported. Error setting: #{setting_name}" end - yield category, setting_name, value, hash.symbolize_keys! + if hash['hidden']&.is_a?(Hash) + raise Discourse::Deprecation, "Hidden site setting per env is no longer supported. Error setting: #{setting_name}" + end + + yield category, setting_name, value, hash.deep_symbolize_keys! else # Simplest case. site_setting_name: 'default value' yield category, setting_name, hash, {} diff --git a/plugins/discourse-narrative-bot/config/settings.yml b/plugins/discourse-narrative-bot/config/settings.yml index e36e06f8a4d..c33d382ced0 100644 --- a/plugins/discourse-narrative-bot/config/settings.yml +++ b/plugins/discourse-narrative-bot/config/settings.yml @@ -1,8 +1,6 @@ plugins: discourse_narrative_bot_enabled: - default: - default: true - test: false + default: true client: true disable_discourse_narrative_bot_welcome_post: default: false diff --git a/script/benchmarks/site_setting/bench.rb b/script/benchmarks/site_setting/bench.rb new file mode 100644 index 00000000000..1ac30060130 --- /dev/null +++ b/script/benchmarks/site_setting/bench.rb @@ -0,0 +1,48 @@ +require 'benchmark/ips' +require File.expand_path('../../../../config/environment', __FILE__) + +# Put pre conditions here +# Used db but it's OK in the most cases + +# build the cache +SiteSetting.title = SecureRandom.hex +SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' +SiteSetting.refresh! + +tests = [ + ["current cache", lambda do + SiteSetting.title + SiteSetting.enable_sso + end + ], + ["change default locale with current cache refreshed", lambda do + SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' + end + ], + ["change site setting", lambda do + SiteSetting.title = SecureRandom.hex + end + ], +] + +Benchmark.ips do |x| + tests.each do |test, proc| + x.report(test, proc) + end +end + +# 2017-08-02 - Erick's Site Setting change + +# Before +# Calculating ------------------------------------- +# current cache 167.518k (±12.1%) i/s - 822.983k in 5.000478s +# change default locale with current cache refreshed +# 174.173 (±16.7%) i/s - 845.000 in 5.015281s +# change site setting 132.956 (±16.5%) i/s - 663.000 in 5.124766s + +# After +# Calculating ------------------------------------- +# current cache 167.170k (±12.2%) i/s - 824.688k in 5.022784s +# change default locale with current cache refreshed +# 79.876 (±16.3%) i/s - 392.000 in 5.067448s +# change site setting 129.085 (±13.2%) i/s - 636.000 in 5.032536s diff --git a/script/benchmarks/site_setting/profile.rb b/script/benchmarks/site_setting/profile.rb new file mode 100644 index 00000000000..7b7237055d9 --- /dev/null +++ b/script/benchmarks/site_setting/profile.rb @@ -0,0 +1,32 @@ +require 'ruby-prof' + +def profile(&blk) + result = RubyProf.profile(&blk) + printer = RubyProf::GraphHtmlPrinter.new(result) + printer.print(STDOUT) +end +profile { '' } # loading profiler dependency + +require File.expand_path('../../../../config/environment', __FILE__) + +# warming up +SiteSetting.title +SiteSetting.enable_sso +SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' +SiteSetting.title = SecureRandom.hex + +profile do + SiteSetting.title +end + +profile do + SiteSetting.enable_sso +end + +profile do + SiteSetting.default_locale = SiteSetting.default_locale == 'en' ? 'zh_CN' : 'en' +end + +profile do + SiteSetting.title = SecureRandom.hex +end diff --git a/spec/components/site_setting_extension_spec.rb b/spec/components/site_setting_extension_spec.rb index 851ceec026e..5d87e8f9838 100644 --- a/spec/components/site_setting_extension_spec.rb +++ b/spec/components/site_setting_extension_spec.rb @@ -64,7 +64,7 @@ describe SiteSettingExtension do expect(settings.hello).to eq(99) end - it "Publishes changes cross sites" do + it "publishes changes cross sites" do settings.setting(:hello, 1) settings2.setting(:hello, 1) @@ -143,7 +143,7 @@ describe SiteSettingExtension do it "should publish changes to clients" do settings.setting("test_setting", 100) - settings.client_setting("test_setting") + settings.setting("test_setting", nil, client: true) messages = MessageBus.track_publish do settings.test_setting = 88 @@ -155,8 +155,11 @@ describe SiteSettingExtension do end describe "remove_override" do - it "correctly nukes overrides" do + before do settings.setting(:test_override, "test") + settings.refresh! + end + it "correctly nukes overrides" do settings.test_override = "bla" settings.remove_override!(:test_override) expect(settings.test_override).to eq("test") @@ -263,6 +266,7 @@ describe SiteSettingExtension do settings.setting(:test_int_enum, 1, enum: TestIntEnumClass) settings.test_int_enum = "2" settings.refresh! + expect(settings.defaults[:test_int_enum]).to eq(1) expect(settings.test_int_enum).to eq(2) end @@ -272,7 +276,7 @@ describe SiteSettingExtension do class TestEnumClass def self.valid_value?(v) - true + self.values.include?(v) end def self.values ['en'] @@ -299,6 +303,10 @@ describe SiteSettingExtension do expect(settings.all_settings.detect { |s| s[:setting] == :test_enum }).to be_present end + it 'should report error when being set other values' do + expect { settings.test_enum = 'not_in_enum' }.to raise_error(Discourse::InvalidParameters) + end + context 'when overridden' do after :each do settings.remove_override!(:validated_setting) @@ -384,6 +392,14 @@ describe SiteSettingExtension do end end + describe ".set_and_log" do + it "raises an error when set for an invalid setting name" do + expect { + settings.set_and_log("provider", "haxxed") + }.to raise_error(ArgumentError) + end + end + describe "filter domain name" do before do settings.setting(:white_listed_spam_host_domains, "www.example.com") @@ -503,4 +519,62 @@ describe SiteSettingExtension do end end + describe 'locale default overrides are respected' do + before do + settings.setting(:test_override, 'default', locale_default: { zh_CN: 'cn' }) + settings.refresh! + end + + after do + settings.remove_override!(:test_override) + end + + it 'ensures the default cache expired after overriding the default_locale' do + expect(settings.test_override).to eq('default') + settings.default_locale = 'zh_CN' + expect(settings.test_override).to eq('cn') + end + + it 'returns the saved setting even locale default exists' do + expect(settings.test_override).to eq('default') + settings.default_locale = 'zh_CN' + settings.test_override = 'saved' + expect(settings.test_override).to eq('saved') + end + end + + describe '.requires_refresh?' do + it 'always refresh default_locale always require refresh' do + expect(settings.requires_refresh?(:default_locale)).to be_truthy + end + end + + describe '.default_locale' do + it 'is always loaded' do + expect(settings.default_locale).to eq 'en' + end + end + + describe '.default_locale=' do + it 'can be changed' do + settings.default_locale = 'zh_CN' + expect(settings.default_locale).to eq 'zh_CN' + end + + it 'refresh!' do + settings.expects(:refresh!) + settings.default_locale = 'zh_CN' + end + + it 'expires the cache' do + settings.default_locale = 'zh_CN' + expect(Rails.cache.exist?(SiteSettingExtension.client_settings_cache_key)).to be_falsey + end + + it 'refreshes the client' do + Discourse.expects(:request_refresh!) + settings.default_locale = 'zh_CN' + end + end + end diff --git a/spec/components/site_settings/defaults_provider_spec.rb b/spec/components/site_settings/defaults_provider_spec.rb new file mode 100644 index 00000000000..01239fbff9b --- /dev/null +++ b/spec/components/site_settings/defaults_provider_spec.rb @@ -0,0 +1,268 @@ +require 'rails_helper' +require_dependency 'site_settings/defaults_provider' + +describe SiteSettings::DefaultsProvider do + + let :provider_local do + SiteSettings::LocalProcessProvider.new + end + + def new_settings(provider) + Class.new do + extend SiteSettingExtension + self.provider = provider + end + end + + let :settings do + new_settings(provider_local) + end + + describe 'inserts default_locale into refresh' do + it 'when initialize' do + expect(settings.refresh_settings.include?(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY)).to be_truthy + end + end + + describe '.db_all' do + it 'collects values from db except default locale' do + settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, + 'en', + SiteSetting.types[:string]) + expect(settings.defaults.db_all).to eq([]) + end + + it 'can collect values from db' do + settings.provider.save('try_a', 1, SiteSetting.types[:integer]) + settings.provider.save('try_b', 2, SiteSetting.types[:integer]) + expect(settings.defaults.db_all.count).to eq 2 + end + end + + describe 'expose default cache according to locale' do + before(:each) do + settings.setting(:test_override, 'default', locale_default: { zh_CN: 'cn' }) + settings.setting(:test_default, 'test', regex: '^\S+$') + settings.refresh! + end + + describe '.all' do + it 'returns all values according to the current locale' do + expect(settings.defaults.all).to eq(test_override: 'default', test_default: 'test') + settings.defaults.site_locale = 'zh_CN' + settings.defaults.refresh_site_locale! + expect(settings.defaults.all).to eq(test_override: 'cn', test_default: 'test') + end + end + + describe '.get' do + it 'returns the default value to a site setting' do + expect(settings.defaults.get(:test_override)).to eq 'default' + end + + it 'accepts a string as the parameters' do + expect(settings.defaults.get('test_override')).to eq 'default' + end + + it 'returns the default value according to current locale' do + expect(settings.defaults.get(:test_override)).to eq 'default' + settings.defaults.site_locale = 'zh_CN' + expect(settings.defaults.get(:test_override)).to eq 'cn' + end + end + + describe '.set_regardless_of_locale' do + let(:val) { 'env_overriden' } + + it 'sets the default value to a site setting regardless the locale' do + settings.defaults.set_regardless_of_locale(:test_override, val) + expect(settings.defaults.get(:test_override)).to eq val + settings.defaults.site_locale = 'zh_CN' + expect(settings.defaults.get(:test_override)).to eq val + end + + it 'handles the string' do + settings.defaults.set_regardless_of_locale('test_override', val) + expect(settings.defaults.get(:test_override)).to eq val + end + + it 'converts the data type' do + settings.defaults.set_regardless_of_locale(:test_override, 1) + expect(settings.defaults.get(:test_override)).to eq '1' + end + + it 'raises when the setting does not exists' do + expect { + settings.defaults.set_regardless_of_locale(:not_exist, 1) + }.to raise_error(ArgumentError) + end + + it 'raises when the value is not valid' do + expect { + settings.defaults.set_regardless_of_locale(:test_default, 'regex will fail') + }.to raise_error(Discourse::InvalidParameters) + end + end + + describe '.each' do + it 'yields the pair of site settings' do + expect { |b| settings.defaults.each(&b) }.to yield_successive_args([:test_override, 'default'], [:test_default, 'test']) + settings.defaults.site_locale = 'zh_CN' + expect { |b| settings.defaults.each(&b) }.to yield_successive_args([:test_override, 'cn'], [:test_default, 'test']) + end + end + end + + describe '.site_locale' do + it 'returns the current site locale' do + expect(settings.defaults.site_locale).to eq 'en' + end + + context 'when locale is set in the db' do + let(:db_val) { 'zr' } + let(:global_val) { 'gr' } + + before do + settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, + db_val, + SiteSetting.types[:string]) + settings.defaults.refresh_site_locale! + end + + it 'should load from database' do + expect(settings.defaults.site_locale).to eq db_val + end + + it 'prioritizes GlobalSetting than value from db' do + GlobalSetting.stubs(:default_locale).returns(global_val) + settings.defaults.refresh_site_locale! + expect(settings.defaults.site_locale).to eq global_val + end + + it 'ignores blank GlobalSetting' do + GlobalSetting.stubs(:default_locale).returns('') + settings.defaults.refresh_site_locale! + expect(settings.defaults.site_locale).to eq db_val + end + end + + end + + describe '.site_locale=' do + it 'changes and store the current site locale' do + settings.defaults.site_locale = 'zh_CN' + expect(settings.defaults.site_locale).to eq 'zh_CN' + end + + it 'changes and store the current site locale' do + expect { settings.defaults.site_locale = 'random' }.to raise_error(Discourse::InvalidParameters) + expect(settings.defaults.site_locale).to eq 'en' + end + + it "don't change when it's shadowed" do + GlobalSetting.stubs(:default_locale).returns('shadowed') + settings.defaults.site_locale = 'zh_CN' + expect(settings.defaults.site_locale).to eq 'shadowed' + end + + it 'refresh_site_locale! when called' do + settings.defaults.expects(:refresh_site_locale!) + settings.defaults.site_locale = 'zh_CN' + end + + it 'refreshes the client when changed' do + Discourse.expects(:request_refresh!).once + settings.defaults.site_locale = 'zh_CN' + end + + it "doesn't refresh the client when changed" do + Discourse.expects(:request_refresh!).never + settings.defaults.site_locale = 'en' + end + end + + describe '.locale_setting_hash' do + it 'returns the hash for client display' do + result = settings.defaults.locale_setting_hash + + expect(result[:setting]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY) + expect(result[:default]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) + expect(result[:type]).to eq(SiteSetting.types[SiteSetting.types[:enum]]) + expect(result[:preview]).to be_nil + expect(result[:value]).to eq(SiteSettings::DefaultsProvider::DEFAULT_LOCALE) + expect(result[:category]).to eq(SiteSettings::DefaultsProvider::DEFAULT_CATEGORY) + expect(result[:valid_values]).to eq(LocaleSiteSetting.values) + expect(result[:translate_names]).to eq(LocaleSiteSetting.translate_names?) + expect(result[:description]).not_to be_nil + end + end + + describe '.load_setting' do + it 'adds a setting to the cache' do + settings.defaults.load_setting('new_a', 1) + expect(settings.defaults[:new_a]).to eq 1 + end + + it 'takes care of locale default' do + settings.defaults.load_setting(:new_b, 1, locale_default: { zh_CN: 2, zh_TW: 2 }) + expect(settings.defaults[:new_b]).to eq 1 + end + end + + describe '.refresh_site_locale!' do + it 'loads the change to locale' do + expect(settings.defaults.site_locale).to eq 'en' + settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, + 'zh_CN', + SiteSetting.types[:string]) + settings.defaults.refresh_site_locale! + expect(settings.defaults.site_locale).to eq 'zh_CN' + end + + it 'loads from GlobalSettings' do + expect(settings.defaults.site_locale).to eq 'en' + GlobalSetting.stubs(:default_locale).returns('fr') + settings.defaults.refresh_site_locale! + expect(settings.defaults.site_locale).to eq 'fr' + end + + it 'prioritized GlobalSettings than db' do + expect(settings.defaults.site_locale).to eq 'en' + settings.provider.save(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY, + 'zh_CN', + SiteSetting.types[:string]) + GlobalSetting.stubs(:default_locale).returns('fr') + settings.defaults.refresh_site_locale! + expect(settings.defaults.site_locale).to eq 'fr' + end + end + + describe '.has_setting?' do + before do + settings.setting(:r, 1) + settings.setting(:question?, 1) + end + + it "returns true when it's present in the cache" do + expect(settings.defaults.has_setting?(:r)).to be_truthy + end + + it '"responds when the arg is string' do + expect(settings.defaults.has_setting?('r')).to be_truthy + end + + it 'default_locale always exists' do + expect(settings.defaults.has_setting?(SiteSettings::DefaultsProvider::DEFAULT_LOCALE_KEY)).to be_truthy + end + + it 'returns false when the key is not exist' do + expect(settings.defaults.has_setting?('no_key')).to be_falsey + end + + it 'checks name with question mark' do + expect(settings.defaults.has_setting?(:question)).to be_truthy + expect(settings.defaults.has_setting?('question')).to be_truthy + end + end + +end diff --git a/spec/components/site_settings/type_supervisor_spec.rb b/spec/components/site_settings/type_supervisor_spec.rb new file mode 100644 index 00000000000..3e4fbae1cd9 --- /dev/null +++ b/spec/components/site_settings/type_supervisor_spec.rb @@ -0,0 +1,341 @@ +require 'rails_helper' +require_dependency 'site_settings/type_supervisor' + +describe SiteSettings::TypeSupervisor do + let :provider_local do + SiteSettings::LocalProcessProvider.new + end + + def new_settings(provider) + Class.new do + extend SiteSettingExtension + self.provider = provider + end + end + + let :settings do + new_settings(provider_local) + end + + subject { SiteSettings::TypeSupervisor } + + describe 'constants' do + it 'validator opts are the subset of consumed opts' do + expect(Set.new(SiteSettings::TypeSupervisor::CONSUMED_OPTS).superset?( + Set.new(SiteSettings::TypeSupervisor::VALIDATOR_OPTS))).to be_truthy + end + end + + describe '#types' do + context "verify enum sequence" do + it "'string' should be at 1st position" do + expect(SiteSettings::TypeSupervisor.types[:string]).to eq(1) + end + it "'time' should be at 2nd position" do + expect(SiteSettings::TypeSupervisor.types[:time]).to eq(2) + end + it "'integer' should be at 3rd position" do + expect(SiteSettings::TypeSupervisor.types[:integer]).to eq(3) + end + it "'float' should be at 4th position" do + expect(SiteSettings::TypeSupervisor.types[:float]).to eq(4) + end + it "'bool' should be at 5th position" do + expect(SiteSettings::TypeSupervisor.types[:bool]).to eq(5) + end + it "'null' should be at 6th position" do + expect(SiteSettings::TypeSupervisor.types[:null]).to eq(6) + end + it "'enum' should be at 7th position" do + expect(SiteSettings::TypeSupervisor.types[:enum]).to eq(7) + end + it "'list' should be at 8th position" do + expect(SiteSettings::TypeSupervisor.types[:list]).to eq(8) + end + it "'url_list' should be at 9th position" do + expect(SiteSettings::TypeSupervisor.types[:url_list]).to eq(9) + end + it "'host_list' should be at 10th position" do + expect(SiteSettings::TypeSupervisor.types[:host_list]).to eq(10) + end + it "'category_list' should be at 11th position" do + expect(SiteSettings::TypeSupervisor.types[:category_list]).to eq(11) + end + it "'value_list' should be at 12th position" do + expect(SiteSettings::TypeSupervisor.types[:value_list]).to eq(12) + end + it "'regex' should be at 13th position" do + expect(SiteSettings::TypeSupervisor.types[:regex]).to eq(13) + end + it "'email' should be at 14th position" do + expect(SiteSettings::TypeSupervisor.types[:email]).to eq(14) + end + it "'username' should be at 15th position" do + expect(SiteSettings::TypeSupervisor.types[:username]).to eq(15) + end + end + end + + describe '#parse_value_type' do + it 'returns :null type when the value is nil' do + expect(subject.parse_value_type(nil)).to eq(SiteSetting.types[:null]) + end + + it 'returns :integer type when the value is int' do + expect(subject.parse_value_type(2)).to eq(SiteSetting.types[:integer]) + end + + it 'returns :integer type when the value is large int' do + expect(subject.parse_value_type(99999999999999999999999999999999999)).to eq(SiteSetting.types[:integer]) + end + + it 'returns :float type when the value is float' do + expect(subject.parse_value_type(1.23)).to eq(SiteSetting.types[:float]) + end + + it 'returns :bool type when the value is true' do + expect(subject.parse_value_type(true)).to eq(SiteSetting.types[:bool]) + end + + it 'returns :bool type when the value is false' do + expect(subject.parse_value_type(false)).to eq(SiteSetting.types[:bool]) + end + + it 'raises when the value is not listed' do + expect { + subject.parse_value_type(Object.new) + }.to raise_error ArgumentError + end + + end + + context 'with different data types' do + class TestEnumClass + def self.valid_value?(v) + self.values.include?(v) + end + def self.values + ['en'] + end + def self.translate_names? + false + end + end + + class TestSmallThanTenValidator + def initialize(opts) + end + def valid_value?(v) + v < 10 + end + def error_message + '' + end + end + + before do + settings.setting(:type_null, nil) + settings.setting(:type_int, 1) + settings.setting(:type_true, true) + settings.setting(:type_false, false) + settings.setting(:type_float, 2.3232) + settings.setting(:type_string, 'string') + settings.setting(:type_enum_default_string, '2', type: 'enum', choices: ['2']) + settings.setting(:type_enum_class, 'en', enum: 'TestEnumClass') + settings.setting(:type_validator, 5, validator: 'TestSmallThanTenValidator') + settings.setting(:type_mock_validate_method, 'no_value') + settings.setting(:type_custom, 'custom', type: 'list') + settings.refresh! + end + + describe '.to_db_value' do + let(:true_val) { 't' } + let(:false_val) { 'f' } + + it 'returns nil value' do + expect(settings.type_supervisor.to_db_value(:type_null, nil)).to eq [nil, SiteSetting.types[:null]] + end + + it 'gives a second chance to guess even told :null type' do + expect(settings.type_supervisor.to_db_value(:type_null, 1)).to eq [1, SiteSetting.types[:integer]] + end + + it 'writes `t` or `f` given the possible bool value' do + expect(settings.type_supervisor.to_db_value(:type_true, true)).to eq [true_val, SiteSetting.types[:bool]] + expect(settings.type_supervisor.to_db_value(:type_true, 't')).to eq [true_val, SiteSetting.types[:bool]] + expect(settings.type_supervisor.to_db_value(:type_true, 'true')).to eq [true_val, SiteSetting.types[:bool]] + expect(settings.type_supervisor.to_db_value(:type_true, false)).to eq [false_val, SiteSetting.types[:bool]] + end + + it 'writes `f` if given not `true` value' do + expect(settings.type_supervisor.to_db_value(:type_true, '')).to eq [false_val, SiteSetting.types[:bool]] + expect(settings.type_supervisor.to_db_value(:type_true, nil)).to eq [false_val, SiteSetting.types[:bool]] + end + + it 'returns floats value' do + expect(settings.type_supervisor.to_db_value(:type_float, 1.2)).to eq [1.2, SiteSetting.types[:float]] + expect(settings.type_supervisor.to_db_value(:type_float, 1)).to eq [1.0, SiteSetting.types[:float]] + end + + it 'returns string value' do + expect(settings.type_supervisor.to_db_value(:type_string, 'a')).to eq ['a', SiteSetting.types[:string]] + end + + it 'returns enum value with string default' do + expect(settings.type_supervisor.to_db_value(:type_enum_default_string, 2)).to eq ['2', SiteSetting.types[:enum]] + expect(settings.type_supervisor.to_db_value(:type_enum_default_string, '2')).to eq ['2', SiteSetting.types[:enum]] + end + + it 'raises when it does not in the enum choices' do + expect { + settings.type_supervisor.to_db_value(:type_enum_default_string, 'random') + }.to raise_error Discourse::InvalidParameters + end + + it 'returns enum value for the given enum class' do + expect(settings.type_supervisor.to_db_value(:type_enum_class, 'en')).to eq ['en', SiteSetting.types[:enum]] + end + + it 'raises when it does not in the enum class' do + expect { + settings.type_supervisor.to_db_value(:type_enum_class, 'random') + }.to raise_error Discourse::InvalidParameters + end + + it 'validates value by validator' do + expect(settings.type_supervisor.to_db_value(:type_validator, 1)).to eq [1, SiteSetting.types[:integer]] + end + + it 'raises when the validator says so' do + expect { + settings.type_supervisor.to_db_value(:type_validator, 11) + }.to raise_error Discourse::InvalidParameters + end + + it 'tries invoke validate methods' do + settings.type_supervisor.expects(:validate_type_mock_validate_method).with('no') + settings.type_supervisor.to_db_value(:type_mock_validate_method, 'no') + end + end + + describe '.to_rb_value' do + let(:true_val) { 't' } + let(:false_val) { 'f' } + + it 'the type can be overriden by a parameter' do + expect(settings.type_supervisor.to_rb_value(:type_null, '1', SiteSetting.types[:integer])).to eq(1) + end + + it 'returns nil value' do + expect(settings.type_supervisor.to_rb_value(:type_null, '1')).to eq nil + expect(settings.type_supervisor.to_rb_value(:type_null, 1)).to eq nil + expect(settings.type_supervisor.to_rb_value(:type_null, 'null')).to eq nil + expect(settings.type_supervisor.to_rb_value(:type_null, 'nil')).to eq nil + end + + it 'returns true when it is true or `t` or `true`' do + expect(settings.type_supervisor.to_rb_value(:type_true, true)).to eq true + expect(settings.type_supervisor.to_rb_value(:type_true, 't')).to eq true + expect(settings.type_supervisor.to_rb_value(:type_true, 'true')).to eq true + end + + it 'returns false if not one of `true` value' do + expect(settings.type_supervisor.to_rb_value(:type_true, 'tr')).to eq false + expect(settings.type_supervisor.to_rb_value(:type_true, '')).to eq false + expect(settings.type_supervisor.to_rb_value(:type_true, nil)).to eq false + expect(settings.type_supervisor.to_rb_value(:type_true, false)).to eq false + expect(settings.type_supervisor.to_rb_value(:type_true, 'f')).to eq false + expect(settings.type_supervisor.to_rb_value(:type_true, 'false')).to eq false + end + + it 'returns float value' do + expect(settings.type_supervisor.to_rb_value(:type_float, 1.2)).to eq 1.2 + expect(settings.type_supervisor.to_rb_value(:type_float, 1)).to eq 1.0 + expect(settings.type_supervisor.to_rb_value(:type_float, '2.2')).to eq 2.2 + expect(settings.type_supervisor.to_rb_value(:type_float, '2')).to eq 2 + end + + it 'returns string value' do + expect(settings.type_supervisor.to_rb_value(:type_string, 'a')).to eq 'a' + expect(settings.type_supervisor.to_rb_value(:type_string, 2)).to eq '2' + end + + it 'returns value with string default' do + expect(settings.type_supervisor.to_rb_value(:type_enum_default_string, 2)).to eq '2' + expect(settings.type_supervisor.to_rb_value(:type_enum_default_string, '2')).to eq '2' + end + + it 'returns value with a custom type' do + expect(settings.type_supervisor.to_rb_value(:type_custom, 2)).to eq 2 + expect(settings.type_supervisor.to_rb_value(:type_custom, '2|3')).to eq '2|3' + end + end + end + + describe '.type_hash' do + class TestEnumClass2 + def self.valid_value?(v) + self.values.include?(v) + end + def self.values + ['a', 'b'] + end + def self.translate_names? + false + end + end + + before do + settings.setting(:type_null, nil) + settings.setting(:type_int, 1) + settings.setting(:type_true, true) + settings.setting(:type_float, 2.3232) + settings.setting(:type_string, 'string') + settings.setting(:type_url_list, 'string', type: 'url_list') + settings.setting(:type_enum_choices, '2', type: 'enum', choices: ['1', '2']) + settings.setting(:type_enum_class, 'a', enum: 'TestEnumClass2') + settings.setting(:type_list, 'a', type: 'list', choices: ['a', 'b']) + settings.refresh! + end + + it 'returns null type' do + expect(settings.type_supervisor.type_hash(:type_null)[:type]).to eq 'null' + end + it 'returns int type' do + expect(settings.type_supervisor.type_hash(:type_int)[:type]).to eq 'integer' + end + it 'returns bool type' do + expect(settings.type_supervisor.type_hash(:type_true)[:type]).to eq 'bool' + end + it 'returns float type' do + expect(settings.type_supervisor.type_hash(:type_float)[:type]).to eq 'float' + end + it 'returns string type' do + expect(settings.type_supervisor.type_hash(:type_string)[:type]).to eq 'string' + end + it 'returns url_list type' do + expect(settings.type_supervisor.type_hash(:type_url_list)[:type]).to eq 'url_list' + end + it 'returns enum type' do + expect(settings.type_supervisor.type_hash(:type_enum_choices)[:type]).to eq 'enum' + end + + it 'returns list choices' do + expect(settings.type_supervisor.type_hash(:type_list)[:choices]).to eq ['a', 'b'] + end + + it 'returns enum choices' do + hash = settings.type_supervisor.type_hash(:type_enum_choices) + expect(hash[:valid_values]).to eq [{ name: '1', value: '1' }, { name: '2', value: '2' }] + expect(hash[:translate_names]).to eq false + end + + it 'returns enum class' do + hash = settings.type_supervisor.type_hash(:type_enum_class) + expect(hash[:valid_values]).to eq ['a', 'b'] + expect(hash[:translate_names]).to eq false + end + + end + +end diff --git a/spec/components/site_settings/yaml_loader_spec.rb b/spec/components/site_settings/yaml_loader_spec.rb index 7354cb04fd4..52e1952999f 100644 --- a/spec/components/site_settings/yaml_loader_spec.rb +++ b/spec/components/site_settings/yaml_loader_spec.rb @@ -8,26 +8,18 @@ describe SiteSettings::YamlLoader do def load_yaml(file_arg) SiteSettings::YamlLoader.new(file_arg).load do |category, name, default, opts| - if opts.delete(:client) - client_setting(category, name, default, opts) - else - setting(category, name, default, opts) - end + setting(category, name, default, opts) end end def setting(category, name, default = nil, opts = {}) @settings ||= [] + @client_settings ||= [] @settings << name @categories ||= [] @categories << category @categories.uniq! - end - - def client_setting(category, name, default = nil) - @client_settings ||= [] - @client_settings << name - setting(category, name, default) + @client_settings << name if opts.has_key?(:client) end end @@ -36,7 +28,9 @@ describe SiteSettings::YamlLoader do let(:client) { "#{Rails.root}/spec/fixtures/site_settings/client.yml" } let(:enum) { "#{Rails.root}/spec/fixtures/site_settings/enum.yml" } let(:enum_client) { "#{Rails.root}/spec/fixtures/site_settings/enum_client.yml" } - let(:env) { "#{Rails.root}/spec/fixtures/site_settings/env.yml" } + let(:deprecated_env) { "#{Rails.root}/spec/fixtures/site_settings/deprecated_env.yml" } + let(:deprecated_hidden) { "#{Rails.root}/spec/fixtures/site_settings/deprecated_hidden.yml" } + let(:locale_default) { "#{Rails.root}/spec/fixtures/site_settings/locale_default.yml" } it "loads simple settings" do receiver.expects(:setting).with('category1', 'title', 'My Site', {}).once @@ -57,9 +51,9 @@ describe SiteSettings::YamlLoader do end it "can load client settings" do - receiver.expects(:client_setting).with('category1', 'title', 'Discourse', {}) - receiver.expects(:client_setting).with('category2', 'tos_url', '', {}) - receiver.expects(:client_setting).with('category2', 'must_approve_users', false, {}) + receiver.expects(:setting).with('category1', 'title', 'Discourse', client: true) + receiver.expects(:setting).with('category2', 'tos_url', '', client: true) + receiver.expects(:setting).with('category2', 'must_approve_users', false, client: true) receiver.load_yaml(client) end @@ -69,15 +63,22 @@ describe SiteSettings::YamlLoader do end it "can load enum client settings" do - receiver.expects(:client_setting).with do |category, name, default, opts| - category == ('basics') && name == ('default_locale') && default == ('en') && opts[:enum] == ('LocaleSiteSetting') + receiver.expects(:setting).with do |category, name, default, opts| + category == ('basics') && name == ('default_locale') && default == ('en') && opts[:enum] == ('LocaleSiteSetting') && opts[:client] == true end receiver.load_yaml(enum_client) end - it "can load settings based on environment" do - receiver.expects(:setting).with('misc', 'port', '', {}) - receiver.expects(:client_setting).with('misc', 'crawl_images', false, {}) - receiver.load_yaml(env) + it "raises deprecation when load settings based on environment" do + expect { receiver.load_yaml(deprecated_env) }.to raise_error(Discourse::Deprecation) + end + + it "raises deprecation when hidden property is based on environment" do + expect { receiver.load_yaml(deprecated_hidden) }.to raise_error(Discourse::Deprecation) + end + + it "can load settings with locale default" do + receiver.expects(:setting).with('search', 'min_search_term_length', 3, min: 2, client: true, locale_default: { zh_CN: 2, zh_TW: 2 }) + receiver.load_yaml(locale_default) end end diff --git a/spec/controllers/admin/site_settings_controller_spec.rb b/spec/controllers/admin/site_settings_controller_spec.rb index 7a2de2d6d0f..6193572987e 100644 --- a/spec/controllers/admin/site_settings_controller_spec.rb +++ b/spec/controllers/admin/site_settings_controller_spec.rb @@ -27,6 +27,7 @@ describe Admin::SiteSettingsController do before do SiteSetting.setting(:test_setting, "default") + SiteSetting.refresh! end it 'sets the value when the param is present' do @@ -49,6 +50,7 @@ describe Admin::SiteSettingsController do it 'does not allow changing of hidden settings' do SiteSetting.setting(:hidden_setting, "hidden", hidden: true) + SiteSetting.refresh! result = xhr :put, :update, id: 'hidden_setting', hidden_setting: 'not allowed' expect(SiteSetting.hidden_setting).to eq("hidden") expect(result.status).to eq(422) diff --git a/spec/fixtures/site_settings/env.yml b/spec/fixtures/site_settings/deprecated_env.yml similarity index 100% rename from spec/fixtures/site_settings/env.yml rename to spec/fixtures/site_settings/deprecated_env.yml diff --git a/spec/fixtures/site_settings/deprecated_hidden.yml b/spec/fixtures/site_settings/deprecated_hidden.yml new file mode 100644 index 00000000000..ba483cbb0db --- /dev/null +++ b/spec/fixtures/site_settings/deprecated_hidden.yml @@ -0,0 +1,6 @@ +developer: + force_hostname: + hidden: + development: false + default: true + default: '' diff --git a/spec/fixtures/site_settings/locale_default.yml b/spec/fixtures/site_settings/locale_default.yml new file mode 100644 index 00000000000..9da10c8f39d --- /dev/null +++ b/spec/fixtures/site_settings/locale_default.yml @@ -0,0 +1,8 @@ +search: + min_search_term_length: + client: true + default: 3 + locale_default: + zh_CN: 2 + zh_TW: 2 + min: 2 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 986bde936c9..0a983d995bd 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -80,8 +80,8 @@ Spork.prefork do # and pretend they are default. # There are a bunch of settings that are seeded, they must be loaded as defaults SiteSetting.current.each do |k, v| - # skip setting defauls for settings that are in unloaded plugins - SiteSetting.defaults[k] = v if SiteSetting.respond_to? k + # skip setting defaults for settings that are in unloaded plugins + SiteSetting.defaults.set_regardless_of_locale(k, v) if SiteSetting.respond_to? k end require_dependency 'site_settings/local_process_provider' @@ -119,6 +119,7 @@ Spork.prefork do SiteSetting.provider.all.each do |setting| SiteSetting.remove_override!(setting.name) end + SiteSetting.defaults.site_locale = SiteSettings::DefaultsProvider::DEFAULT_LOCALE # very expensive IO operations SiteSetting.automatically_download_gravatars = false