Class: Magick::ExportImport

Inherits:
Object
  • Object
show all
Defined in:
lib/magick/export_import.rb

Defined Under Namespace

Classes: ImportError

Constant Summary collapse

DEFAULT_MAX_IMPORT_FEATURES =

Hard cap on the number of features accepted by a single import call. Guards against DoS via an oversized payload and (combined with Admin UI auth) stops an attacker from using import as a flag-replacement primitive. Override with the MAGICK_MAX_IMPORT_FEATURES env var.

10_000

Class Method Summary collapse

Class Method Details

.apply_dependencies(feature, deps) ⇒ Object

A payload without a "dependencies" key leaves the feature's stored prerequisites alone; an explicit list (including []) replaces them, so an export taken after a dependency was removed re-applies that removal.



147
148
149
150
151
152
153
# File 'lib/magick/export_import.rb', line 147

def self.apply_dependencies(feature, deps)
  return if deps.nil?

  feature.replace_dependencies(deps)
rescue ArgumentError => e
  raise ImportError, "Magick.import: invalid dependencies for '#{feature.name}': #{e.message}"
end

.apply_targeting(feature, targeting) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/magick/export_import.rb', line 109

def self.apply_targeting(feature, targeting)
  return unless targeting.is_a?(Hash)

  # Variants live under the internal :variants key of the targeting hash
  # but are not part of the targeting wire payload, so they are stripped
  # here and applied separately by apply_variants.
  payload = targeting.reject { |key, _| key.to_s == 'variants' }
  return if payload.empty?

  feature.replace_targeting(payload)
rescue Magick::InvalidTargetingError => e
  raise ImportError, "Magick.import: invalid targeting for '#{feature.name}': #{e.message}"
end

.apply_value(feature, feature_data) ⇒ Object



104
105
106
107
# File 'lib/magick/export_import.rb', line 104

def self.apply_value(feature, feature_data)
  value = fetch(feature_data, :value)
  feature.set_value(value) if !value.nil? && !(value.is_a?(String) && value.empty?)
end

.apply_variants(feature, feature_data, targeting) ⇒ Object

Variants are read from the top-level key, falling back to the targeting hash for exports written by gem versions whose top-level list was always empty. The payload is the imported feature's whole state, so one without variants must not inherit the variants a same-named feature already has in the target store.



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/magick/export_import.rb', line 128

def self.apply_variants(feature, feature_data, targeting)
  source = Array(fetch(feature_data, :variants))
  source = Array(fetch(targeting, :variants)) if source.empty?

  list = source.filter_map do |variant|
    next unless variant.is_a?(Hash)

    v = variant.transform_keys(&:to_sym)
    next unless v[:name]

    { name: v[:name], value: v[:value], weight: v[:weight] }
  end

  list.empty? ? feature.clear_variants : feature.set_variants(list)
end

.build_feature(name, feature_data, adapter_registry) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/magick/export_import.rb', line 91

def self.build_feature(name, feature_data, adapter_registry)
  Feature.new(
    name,
    adapter_registry,
    type: (fetch(feature_data, :type) || :boolean).to_sym,
    status: (fetch(feature_data, :status) || :active).to_sym,
    default_value: fetch(feature_data, :default_value),
    description: fetch(feature_data, :description),
    display_name: fetch(feature_data, :display_name),
    group: fetch(feature_data, :group)
  )
end

.export(features_hash) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/magick/export_import.rb', line 19

def self.export(features_hash)
  result = features_hash.map do |_name, feature|
    feature.to_h
  end

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.exported(format: :hash, feature_count: result.length)
  end

  result
end

.export_json(features_hash) ⇒ Object



31
32
33
34
35
36
37
38
39
# File 'lib/magick/export_import.rb', line 31

def self.export_json(features_hash)
  result = JSON.pretty_generate(export(features_hash))

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.exported(format: :json, feature_count: features_hash.length)
  end

  result
end

.fetch(hash, key) ⇒ Object



80
81
82
83
84
85
86
87
88
89
# File 'lib/magick/export_import.rb', line 80

def self.fetch(hash, key)
  # Must not use `||` — falsy legitimate values (false, 0, "") would
  # silently fall through to the string-key lookup (and then to nil).
  return hash[key] if hash.key?(key)

  string_key = key.to_s
  return hash[string_key] if hash.key?(string_key)

  nil
end

.import(data, adapter_registry) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/magick/export_import.rb', line 41

def self.import(data, adapter_registry)
  features = {}
  data = JSON.parse(data) if data.is_a?(String)
  list = Array(data)

  cap = max_import_features
  if list.size > cap
    raise ImportError,
          "Magick.import: refused to import #{list.size} features; limit is #{cap}. " \
          'Set MAGICK_MAX_IMPORT_FEATURES to override.'
  end

  list.each do |feature_data|
    unless feature_data.is_a?(Hash)
      raise ImportError, "Magick.import: each feature payload must be a Hash, got #{feature_data.class}"
    end

    name = fetch(feature_data, :name)
    next unless name

    targeting = fetch(feature_data, :targeting)
    targeting = {} unless targeting.is_a?(Hash)

    feature = build_feature(name, feature_data, adapter_registry)
    apply_value(feature, feature_data)
    apply_targeting(feature, targeting)
    apply_variants(feature, feature_data, targeting)
    apply_dependencies(feature, fetch(feature_data, :dependencies))

    features[name.to_s] = feature
  end

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.imported(format: :json, feature_count: features.length)
  end

  features
end

.max_import_features ⇒ Object



15
16
17
# File 'lib/magick/export_import.rb', line 15

def self.max_import_features
  ENV.fetch('MAGICK_MAX_IMPORT_FEATURES', DEFAULT_MAX_IMPORT_FEATURES).to_i
end