setting.rb 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. # Copyright (C) 2012-2024 Zammad Foundation, https://zammad-foundation.org/
  2. # Class variables are used here as performance optimization.
  3. # Technically it is not thread-safe, but it never caused issues.
  4. # rubocop:disable Style/ClassVars
  5. class Setting < ApplicationModel
  6. store :options
  7. store :state_current
  8. store :state_initial
  9. store :preferences
  10. before_validation :state_check
  11. before_create :set_initial
  12. after_save :reset_class_cache_key
  13. after_commit :reset_other_caches, :broadcast_frontend, :check_refresh
  14. validates_with Setting::Validator
  15. attr_accessor :state
  16. @@current = {}
  17. @@raw = {}
  18. @@query_cache_key = nil
  19. @@last_changed_at = nil
  20. @@lookup_at = nil
  21. @@lookup_timeout = if ENV['ZAMMAD_SETTING_TTL']
  22. ENV['ZAMMAD_SETTING_TTL'].to_i.seconds
  23. else
  24. 15.seconds
  25. end
  26. =begin
  27. set config setting
  28. Setting.set('some_config_name', some_value)
  29. =end
  30. def self.set(name, value)
  31. setting = Setting.find_by(name: name)
  32. if !setting
  33. raise "Can't find config setting '#{name}'"
  34. end
  35. setting.state_current = { value: value }
  36. setting.save!
  37. logger.info "Setting.set('#{name}', #{filter_param(name, value).inspect})"
  38. true
  39. end
  40. =begin
  41. get config setting
  42. value = Setting.get('some_config_name')
  43. =end
  44. def self.get(name)
  45. load
  46. @@current[name].deep_dup # prevents accidental modification of settings in console
  47. end
  48. =begin
  49. reset config setting to default
  50. Setting.reset('some_config_name')
  51. Setting.reset('some_config_name', force) # true|false - force it false per default
  52. =end
  53. def self.reset(name, force = false)
  54. setting = Setting.find_by(name: name)
  55. if !setting
  56. raise "Can't find config setting '#{name}'"
  57. end
  58. return true if !force && setting.state_current == setting.state_initial
  59. setting.state_current = setting.state_initial
  60. setting.save!
  61. logger.info "Setting.reset('#{name}', #{filter_param(name, setting.state_current).inspect})"
  62. true
  63. end
  64. =begin
  65. reload config settings
  66. Setting.reload
  67. =end
  68. def self.reload
  69. @@last_changed_at = nil
  70. load(true)
  71. end
  72. # check if cache is still valid
  73. def self.cache_valid?
  74. # Check if last last lookup was recent enough
  75. if @@lookup_at && @@lookup_at > @@lookup_timeout.ago
  76. # logger.debug "Setting.cache_valid?: cache_id has been set within last #{@@lookup_timeout} seconds"
  77. return true
  78. end
  79. if @@query_cache_key && Setting.reorder(:id).cache_key_with_version == @@query_cache_key
  80. @@lookup_at = Time.current
  81. return true
  82. end
  83. false
  84. end
  85. # Used to mask values of sensitive settings such as passwords, tokens etc.
  86. def self.filter_param(key, value)
  87. @@parameter_filter ||= ActiveSupport::ParameterFilter.new(Rails.application.config.filter_parameters)
  88. @@parameter_filter.filter_param(key, value)
  89. end
  90. private
  91. # load values and cache them
  92. def self.load(force = false)
  93. # check if config is already generated
  94. return false if !force && @@current.present? && cache_valid?
  95. # read all or only changed since last read
  96. latest = Setting.maximum(:updated_at)
  97. base_query = Setting.reorder(:id)
  98. settings_query = if @@last_changed_at && @@current.present?
  99. base_query.where(updated_at: @@last_changed_at..)
  100. else
  101. base_query
  102. end
  103. settings = settings_query.pluck(:name, :state_current)
  104. @@last_changed_at = [Time.current, latest].min if latest
  105. if settings.present?
  106. settings.each do |setting|
  107. @@raw[setting[0]] = setting[1]['value']
  108. end
  109. @@raw.each do |key, value|
  110. @@current[key] = interpolate_value value
  111. end
  112. end
  113. @@query_cache_key = base_query.cache_key_with_version
  114. @@lookup_at = Time.current
  115. true
  116. end
  117. private_class_method :load
  118. def self.interpolate_value(input)
  119. return input if !input.is_a? String
  120. input.gsub(%r{\#\{config\.(.+?)\}}) do
  121. @@raw[$1].to_s
  122. end
  123. end
  124. private_class_method :interpolate_value
  125. # set initial value in state_initial
  126. def set_initial
  127. self.state_initial = state_current
  128. end
  129. def reset_class_cache_key
  130. @@lookup_at = nil
  131. @@query_cache_key = nil
  132. end
  133. # Resets caches related to the setting in question.
  134. def reset_other_caches
  135. return if preferences[:cache].blank?
  136. Array(preferences[:cache]).each do |key|
  137. Rails.cache.delete(key)
  138. end
  139. end
  140. # Convert state into hash to be able to store it as store.
  141. def state_check
  142. return if state.nil? # allow false value
  143. return if state.try(:key?, :value)
  144. self.state_current = { value: state }
  145. end
  146. # Notify clients about config changes.
  147. def broadcast_frontend
  148. return if !frontend
  149. # Some setting values use interpolation to reference other settings.
  150. # This is applied in `Setting.get`, thus direct reading of the value should be avoided.
  151. value = self.class.get(name)
  152. Sessions.broadcast(
  153. {
  154. event: 'config_update',
  155. data: { name: name, value: value }
  156. },
  157. preferences[:authentication] ? 'authenticated' : 'public'
  158. )
  159. Gql::Subscriptions::ConfigUpdates.trigger(self)
  160. end
  161. # NB: Force users to reload on SAML credentials config changes
  162. # This is needed because the setting is not frontend related,
  163. # so we can't rely on 'config_update_local' mechanism to kick in
  164. # https://github.com/zammad/zammad/issues/4263
  165. def check_refresh
  166. return if ['auth_saml_credentials'].exclude?(name)
  167. AppVersion.set(true, AppVersion::MSG_CONFIG_CHANGED)
  168. end
  169. end
  170. # rubocop:enable Style/ClassVars