mirror of
https://github.com/zammad/zammad
synced 2026-05-24 09:48:36 +00:00
279 lines
6.9 KiB
Ruby
279 lines
6.9 KiB
Ruby
# Copyright (C) 2012-2026 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
|
|
include ChecksClientNotification
|
|
|
|
store :options
|
|
store :state_current
|
|
store :state_initial
|
|
store :preferences
|
|
before_validation :transform
|
|
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, if: -> { !skip_validate }
|
|
|
|
attr_accessor :state, :skip_validate
|
|
|
|
@@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, validate: true)
|
|
setting = Setting.find_by(name: name)
|
|
if !setting
|
|
raise "Can't find config setting '#{name}'"
|
|
end
|
|
|
|
setting.skip_validate = !validate
|
|
|
|
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|
|
|
if key == 'notification_sender'
|
|
@@current[key] = sanitize_notification_sender value
|
|
next
|
|
end
|
|
|
|
@@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
|
|
|
|
def self.sanitize_notification_sender(input)
|
|
return input if !input.is_a? String
|
|
|
|
input.gsub(%r{\#\{config\.(.+?)\}}) do
|
|
placeholder = @@raw[$1].to_s
|
|
|
|
case $1
|
|
when 'fqdn'
|
|
placeholder = placeholder.sub(%r{:\d+$}, '') # strip port from fqdn
|
|
when 'product_name'
|
|
placeholder = "\"#{placeholder.gsub(%r{"}, "''")}\"" # quote and replace double quotes
|
|
end
|
|
|
|
placeholder
|
|
end
|
|
end
|
|
private_class_method :sanitize_notification_sender
|
|
|
|
# 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 (frontend only!).
|
|
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
|
|
|
|
def notify_clients_send(data)
|
|
permissions = preferences['permission']
|
|
|
|
if permissions.present?
|
|
User.with_permissions(permissions).each do |user|
|
|
PushMessages.send_to(user.id, data[:message])
|
|
end
|
|
|
|
return
|
|
end
|
|
|
|
User.with_permissions('admin.*').each do |user|
|
|
PushMessages.send_to(user.id, data[:message])
|
|
end
|
|
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.trigger_browser_reload AppVersion::MSG_CONFIG_CHANGED
|
|
end
|
|
|
|
def transform
|
|
Array(preferences[:transformations])
|
|
.map { |klass| klass.constantize.new(self).run }
|
|
end
|
|
end
|
|
# rubocop:enable Style/ClassVars
|