123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230 |
- # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
- # Class variables are used here as performance optimization.
- # Technically it is not thread-safe, but it never caused issues.
- # rubocop:disable Style/ClassVars
- class Setting < ApplicationModel
- store :options
- store :state_current
- store :state_initial
- store :preferences
- before_validation :state_check
- before_create :set_initial
- after_save :reset_class_cache_key
- after_commit :reset_other_caches, :broadcast_frontend, :check_refresh
- validates_with Setting::Validator
- attr_accessor :state
- @@current = {}
- @@raw = {}
- @@query_cache_key = nil
- @@last_changed_at = nil
- @@lookup_at = nil
- @@lookup_timeout = if ENV['ZAMMAD_SETTING_TTL']
- ENV['ZAMMAD_SETTING_TTL'].to_i.seconds
- else
- 15.seconds
- end
- =begin
- set config setting
- Setting.set('some_config_name', some_value)
- =end
- def self.set(name, value)
- setting = Setting.find_by(name: name)
- if !setting
- raise "Can't find config setting '#{name}'"
- end
- setting.state_current = { value: value }
- setting.save!
- logger.info "Setting.set('#{name}', #{filter_param(name, value).inspect})"
- true
- end
- =begin
- get config setting
- value = Setting.get('some_config_name')
- =end
- def self.get(name)
- load
- @@current[name].deep_dup # prevents accidental modification of settings in console
- end
- =begin
- reset config setting to default
- Setting.reset('some_config_name')
- Setting.reset('some_config_name', force) # true|false - force it false per default
- =end
- def self.reset(name, force = false)
- setting = Setting.find_by(name: name)
- if !setting
- raise "Can't find config setting '#{name}'"
- end
- return true if !force && setting.state_current == setting.state_initial
- setting.state_current = setting.state_initial
- setting.save!
- logger.info "Setting.reset('#{name}', #{filter_param(name, setting.state_current).inspect})"
- true
- end
- =begin
- reload config settings
- Setting.reload
- =end
- def self.reload
- @@last_changed_at = nil
- load(true)
- end
- # check if cache is still valid
- def self.cache_valid?
- # Check if last last lookup was recent enough
- if @@lookup_at && @@lookup_at > @@lookup_timeout.ago
- # logger.debug "Setting.cache_valid?: cache_id has been set within last #{@@lookup_timeout} seconds"
- return true
- end
- if @@query_cache_key && Setting.reorder(:id).cache_key_with_version == @@query_cache_key
- @@lookup_at = Time.current
- return true
- end
- false
- end
- # Used to mask values of sensitive settings such as passwords, tokens etc.
- def self.filter_param(key, value)
- @@parameter_filter ||= ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
- @@parameter_filter.filter_param(key, value)
- end
- private
- # load values and cache them
- def self.load(force = false)
- # check if config is already generated
- return false if !force && @@current.present? && cache_valid?
- # read all or only changed since last read
- latest = Setting.maximum(:updated_at)
- base_query = Setting.reorder(:id)
- settings_query = if @@last_changed_at && @@current.present?
- base_query.where(updated_at: @@last_changed_at..)
- else
- base_query
- end
- settings = settings_query.pluck(:name, :state_current)
- @@last_changed_at = [Time.current, latest].min if latest
- if settings.present?
- settings.each do |setting|
- @@raw[setting[0]] = setting[1]['value']
- end
- @@raw.each do |key, value|
- @@current[key] = interpolate_value value
- end
- end
- @@query_cache_key = base_query.cache_key_with_version
- @@lookup_at = Time.current
- true
- end
- private_class_method :load
- def self.interpolate_value(input)
- return input if !input.is_a? String
- input.gsub(%r{\#\{config\.(.+?)\}}) do
- @@raw[$1].to_s
- end
- end
- private_class_method :interpolate_value
- # set initial value in state_initial
- def set_initial
- self.state_initial = state_current
- end
- def reset_class_cache_key
- @@lookup_at = nil
- @@query_cache_key = nil
- end
- # Resets caches related to the setting in question.
- def reset_other_caches
- return if preferences[:cache].blank?
- Array(preferences[:cache]).each do |key|
- Rails.cache.delete(key)
- end
- end
- # Convert state into hash to be able to store it as store.
- def state_check
- return if state.nil? # allow false value
- return if state.try(:key?, :value)
- self.state_current = { value: state }
- end
- # Notify clients about config changes.
- def broadcast_frontend
- return if !frontend
- # Some setting values use interpolation to reference other settings.
- # This is applied in `Setting.get`, thus direct reading of the value should be avoided.
- value = self.class.get(name)
- Sessions.broadcast(
- {
- event: 'config_update',
- data: { name: name, value: value }
- },
- preferences[:authentication] ? 'authenticated' : 'public'
- )
- Gql::Subscriptions::ConfigUpdates.trigger(self)
- end
- # NB: Force users to reload on SAML credentials config changes
- # This is needed because the setting is not frontend related,
- # so we can't rely on 'config_update_local' mechanism to kick in
- # https://github.com/zammad/zammad/issues/4263
- def check_refresh
- return if ['auth_saml_credentials'].exclude?(name)
- AppVersion.set(true, AppVersion::MSG_CONFIG_CHANGED)
- end
- end
- # rubocop:enable Style/ClassVars
|