theforeman/foreman

View on GitHub
app/models/setting.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'resolv'

class Setting < ApplicationRecord
  audited :except => [:name]
  extend FriendlyId
  friendly_id :name
  include ActiveModel::Validations
  include EncryptValue
  include PermissionName

  TYPES = %w{integer boolean hash array string}
  NONZERO_ATTRS = %w{puppet_interval idle_timeout entries_per_page outofsync_interval}
  # constant BLANK_ATTRS is deprecated and all settings without custom validation allow blank values
  # if you wish to validate non-empty arrays, please add validation through the new setting DSL
  BLANK_ATTRS = %w{}
  ARRAY_HOSTNAMES = %w{trusted_hosts}
  URI_ATTRS = %w{foreman_url unattended_url}
  URI_BLANK_ATTRS = %w{login_delegation_logout_url}
  IP_ATTRS = %w{libvirt_default_console_address}
  REGEXP_ATTRS = %w{}
  EMAIL_ATTRS = %w{administrator email_reply_address}
  NOT_STRIPPED = %w{}

  class ValueValidator < ActiveModel::Validator
    def validate(record)
      record.send("validate_#{record.name}", record)
    end
  end

  validates_lengths_from_database

  validates :name, :presence => true, :uniqueness => true
  validates :value, :numericality => true, :length => {:maximum => 8}, :if => proc { |s| s.settings_type == "integer" }
  validates :value, :numericality => {:greater_than => 0}, :if => proc { |s| NONZERO_ATTRS.include?(s.name) }
  validates :value, :inclusion => {:in => [true, false]}, :if => proc { |s| s.settings_type.to_s == "boolean" }, :allow_nil => true
  validates :value, :url_schema => ['http', 'https'], :if => proc { |s| URI_ATTRS.include?(s.name) }

  validates :value, :url_schema => ['http', 'https'], :if => proc { |s| URI_BLANK_ATTRS.include?(s.name) && s.value.present? }

  validate :validate_host_owner, :if => proc { |s| s.name == "host_owner" }
  validates :value, :format => { :with => Resolv::AddressRegex }, :if => proc { |s| IP_ATTRS.include? s.name }
  validates :value, :regexp => true, :if => proc { |s| REGEXP_ATTRS.include? s.name }
  validates :value, :array_type => true, :if => proc { |s| s.settings_type == "array" }
  validates_with ValueValidator, :if => proc { |s| Foreman.settings.ready? && s.respond_to?("validate_#{s.name}") }
  validates :value, :array_hostnames_ips => true, :if => proc { |s| ARRAY_HOSTNAMES.include? s.name }
  validates :value, :email => true, :if => proc { |s| EMAIL_ATTRS.include? s.name }
  before_save :clear_value_when_default
  validate :validate_frozen_attributes
  before_validation :remove_whitespaces, :if => proc { |s| s.settings_type == "array" }
  # Custom validations are added from SettingManager class
  after_find :readonly_when_overridden
  after_save :refresh_registry_value
  default_scope -> { order(:name) }

  scope :order_by, ->(attr) { except(:order).order(attr) }

  scoped_search :on => :id, :complete_enabled => false, :only_explicit => true, :validator => ScopedSearch::Validators::INTEGER
  scoped_search on: :name, complete_value: :true, operators: ['=', '~']
  scoped_search on: :description, complete_value: :true, operators: ['~']

  delegate :settings_type, :encrypted, :encrypted?, :default, to: :setting_definition, allow_nil: true

  def self.config_file
    'settings.yaml'
  end

  # can't use our own settings
  def self.per_page
    20
  end

  def self.complete_for(search_query, opts = {})
    SettingRegistry::SettingCompleter.auto_complete(Foreman.settings, scoped_search_definition, search_query, opts)
  end

  def self.[](name)
    Foreman.settings[name]
  end

  def self.[]=(name, value)
    Foreman.settings[name] = value
  end

  def self.setting_type_from_value(value_for_type)
    t = value_for_type.class.to_s.downcase
    return t if TYPES.include?(t)
    return "integer" if value_for_type.is_a?(Integer)
    return "boolean" if value_for_type.is_a?(TrueClass) || value_for_type.is_a?(FalseClass)
  end

  def to_param
    name
  end

  def value=(v)
    v = v.to_yaml unless v.nil?
    # the has_attribute is for enabling DB migrations on older versions
    if setting_definition&.encrypted?
      # Don't re-write the attribute if the current encrypted value is identical to the new one
      current_value = self[:value]
      unless is_decryptable?(current_value) && decrypt_field(current_value) == v
        self[:value] = encrypt_field(v)
      end
    else
      self[:value] = v
    end
  end

  def value
    v = self[:value]
    v = decrypt_field(v)
    v.nil? ? default : YAML.safe_load(v, permitted_classes: [Symbol, Pathname])
  end
  alias_method :value_before_type_cast, :value

  def parse_string_value(val)
    case settings_type
    when "boolean"
      boolean = Foreman::Cast.to_bool(val)

      if boolean.nil?
        invalid_value_error _("must be boolean")
      end

      self.value = boolean

    when "integer"
      if val.to_s =~ /\A\d+\Z/
        self.value = val.to_i
      else
        invalid_value_error _("must be integer")
      end

    when "array"
      if val =~ /\A\[.*\]\Z/
        begin
          self.value = YAML.safe_load(val.gsub(/(\,)(\S)/, "\\1 \\2"))
        rescue => e
          invalid_value_error e.to_s
        end
      else
        invalid_value_error _("must be an array")
      end

    when "string", "text", nil
      # string is taken as default setting type for parsing
      self.value = NOT_STRIPPED.include?(name) ? val : val.to_s.strip

    when "hash"
      raise Foreman::SettingValueException, N_("Parsing a hash from a string is not supported")

    else
      raise Foreman::SettingValueException.new(N_("Parsing settings type '%s' from a string is not defined"), settings_type)

    end
    if errors.present?
      raise Foreman::SettingValueException.new(N_("Error parsing value for setting '%(name)s': %(error)s"),
        { 'name' => name, 'error' => errors.full_messages.join(", ") })
    end
    true
  end

  def self.regexp_expand_wildcard_string(string, options = {})
    prefix = options[:prefix] || '\A'
    suffix = options[:suffix] || '\Z'
    prefix + Regexp.escape(string).gsub('\*', '.*').gsub('\?', '.') + suffix
  end

  def self.convert_array_to_regexp(array, regexp_options = {})
    Regexp.new(array.map { |string| regexp_expand_wildcard_string(string, regexp_options) }.join('|'))
  end

  def has_readonly_value?
    SETTINGS.key?(name.to_sym)
  end

  def self.readonly_value(name)
    SETTINGS[name]
  end

  def read_attribute_before_type_cast(attr_name)
    return value if attr_name == :value
    super(attr_name)
  end

  def self.replace_keywords(keyword)
    keyword&.gsub '$VERSION', SETTINGS[:version].version
  end

  # Methods for loading default settings

  def self.default_settings
    []
  end

  def self.load_defaults
    return false unless table_exists?
    Foreman::Deprecation.deprecation_warning('3.4', "subclassing Setting is deprecated '#{name}' should be migrated to setting DSL "\
                                                    'see https://github.com/theforeman/foreman/blob/develop/developer_docs/how_to_create_a_plugin.asciidoc#settings for details')
    default_settings.each do |s|
      t = Setting.setting_type_from_value(s[:default]) || 'string'
      kwargs = s.except(:name).merge(type: t.to_sym, category: name.delete_prefix('Setting::'), context: :deprecated)
      Foreman.settings._add(s[:name], **kwargs)
    end
    true
  end

  def self.select_collection_registry
    Foreman.settings.select_collection_registry
  end

  def self.set(name, description, default, full_name = nil, value = nil, options = {})
    if options.has_key? :collection
      select_collection_registry.add(name, options)
    end
    options[:encrypted] ||= false
    {:name => name, :value => value, :description => description, :default => default, :full_name => full_name, :encrypted => options[:encrypted]}
  end

  def select_collection
    self.class.select_collection_registry.collection_for self
  end

  def self.model_name
    ActiveModel::Name.new(Setting)
  end

  # End methods for loading default settings

  private

  def validate_host_owner
    owner_type_and_id = value
    return if owner_type_and_id.blank?

    OwnerClassifier.classify_owner(owner_type_and_id)
  rescue ArgumentError, ActiveRecord::RecordNotFound => e
    errors.add(:value, e.message)
  end

  def invalid_value_error(error)
    errors.add(:value, _("is invalid: %s") % error)
  end

  def validate_frozen_attributes
    return true if new_record?
    changed_attributes.each do |c, old|
      # Allow settings_type to change at first (from nil) since it gets populated during validation
      if c.to_s == 'name'
        errors.add(c, _("is not allowed to change"))
        return false
      end
    end
    true
  end

  def clear_value_when_default
    if value == default
      self[:value] = nil
    end
  end

  def readonly_when_overridden
    readonly! if !new_record? && has_readonly_value?
  end

  def setting_definition
    return unless Foreman.settings.ready?
    Foreman.settings.find(name)
  end

  def refresh_registry_value
    setting_definition&.tap do |definition|
      definition.updated_at = updated_at
      definition.value_from_db = value
    end
  end

  def remove_whitespaces
    self[:value] = value.each { |a| a.strip! if a.respond_to? :strip! }
  end
end