forme 1.8.0 → 1.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@ module Forme
10
10
  # Wrap the inputs in a <fieldset>. If the :legend
11
11
  # option is given, add a <legend> tag as the first
12
12
  # child of the fieldset.
13
- def call(form, opts)
13
+ def call(form, opts, &block)
14
14
  attr = opts[:attr] ? opts[:attr].dup : {}
15
15
  Forme.attr_classes(attr, 'inputs')
16
16
  if legend = opts[:legend]
@@ -19,7 +19,7 @@ module Forme
19
19
  yield
20
20
  end
21
21
  else
22
- form.tag(:fieldset, attr, &Proc.new)
22
+ form.tag(:fieldset, attr, &block)
23
23
  end
24
24
  end
25
25
  end
@@ -77,4 +77,23 @@ module Forme
77
77
  t
78
78
  end
79
79
  end
80
+
81
+ class Labeler::Span
82
+ Forme.register_transformer(:labeler, :span, new)
83
+
84
+ def call(tag, input)
85
+ label_attr = input.opts[:label_attr]
86
+ label_attr = label_attr ? label_attr.dup : {}
87
+ Forme.attr_classes(label_attr, "label")
88
+ [input.tag(:span, label_attr, input.opts[:label]), tag]
89
+ end
90
+ end
91
+
92
+ class Labeler::Legend
93
+ Forme.register_transformer(:labeler, :legend, new)
94
+
95
+ def call(tag, input)
96
+ [input.tag(:legend, input.opts[:label_attr], input.opts[:label]), tag]
97
+ end
98
+ end
80
99
  end
@@ -8,12 +8,6 @@ module Forme
8
8
  class Serializer
9
9
  Forme.register_transformer(:serializer, :default, new)
10
10
 
11
- # Borrowed from Rack::Utils, map of single character strings to html escaped versions.
12
- ESCAPE_HTML = {"&" => "&amp;", "<" => "&lt;", ">" => "&gt;", "'" => "&#39;", '"' => "&quot;"}
13
-
14
- # A regexp that matches all html characters requiring escaping.
15
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
16
-
17
11
  # Which tags are self closing (such tags ignore children).
18
12
  SELF_CLOSING = [:img, :input]
19
13
 
@@ -43,7 +37,7 @@ module Forme
43
37
  when Raw
44
38
  tag.to_s
45
39
  else
46
- h tag
40
+ Forme.h(tag)
47
41
  end
48
42
  end
49
43
 
@@ -69,11 +63,6 @@ module Forme
69
63
  time.is_a?(Time) ? (time.strftime('%Y-%m-%dT%H:%M:%S') + sprintf(".%03d", time.usec)) : (time.strftime('%Y-%m-%dT%H:%M:%S.') + time.strftime('%N')[0...3])
70
64
  end
71
65
 
72
- # Escape ampersands, brackets and quotes to their HTML/XML entities.
73
- def h(string)
74
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
75
- end
76
-
77
66
  # Join attribute values that are arrays with spaces instead of an empty
78
67
  # string.
79
68
  def attr_value(v)
@@ -23,7 +23,7 @@ module Forme
23
23
  input.tag(@type, input.opts[:wrapper_attr], super)
24
24
  end
25
25
 
26
- [:li, :p, :div, :span, :td].each do |x|
26
+ [:li, :p, :div, :span, :td, :fieldset].each do |x|
27
27
  Forme.register_transformer(:wrapper, x, new(x))
28
28
  end
29
29
  end
data/lib/forme/version.rb CHANGED
@@ -6,7 +6,7 @@ module Forme
6
6
  MAJOR = 1
7
7
 
8
8
  # The minor version of Forme, updated for new feature releases of Forme.
9
- MINOR = 8
9
+ MINOR = 12
10
10
 
11
11
  # The patch version of Forme, updated only for bug fixes from the last
12
12
  # feature release.
data/lib/forme.rb CHANGED
@@ -8,6 +8,33 @@ module Forme
8
8
  class Error < StandardError
9
9
  end
10
10
 
11
+ begin
12
+ require 'cgi/escape'
13
+ # :nocov:
14
+ unless CGI.respond_to?(:escapeHTML) # work around for JRuby 9.1
15
+ CGI = Object.new
16
+ CGI.extend(defined?(::CGI::Escape) ? ::CGI::Escape : ::CGI::Util)
17
+ end
18
+ def self.h(value)
19
+ CGI.escapeHTML(value.to_s)
20
+ end
21
+ rescue LoadError
22
+ ESCAPE_TABLE = {'&' => '&amp;', '<' => '&lt;', '>' => '&gt;', '"' => '&quot;', "'" => '&#39;'}.freeze
23
+ ESCAPE_TABLE.each_value(&:freeze)
24
+ if RUBY_VERSION >= '1.9'
25
+ # Escape the following characters with their HTML/XML
26
+ # equivalents.
27
+ def self.h(value)
28
+ value.to_s.gsub(/[&<>"']/, ESCAPE_TABLE)
29
+ end
30
+ else
31
+ def self.h(value)
32
+ value.to_s.gsub(/[&<>"']/){|s| ESCAPE_TABLE[s]}
33
+ end
34
+ end
35
+ end
36
+ # :nocov:
37
+
11
38
  @default_add_blank_prompt = nil
12
39
  @default_config = :default
13
40
  class << self
@@ -45,12 +45,26 @@ class Roda
45
45
  csrf_token
46
46
  end
47
47
 
48
+ options[:csrf] = [csrf_field, token]
48
49
  options[:hidden_tags] ||= []
49
50
  options[:hidden_tags] += [{csrf_field=>token}]
50
51
  end
51
52
 
52
53
  options[:output] = @_out_buf if block
53
- ::Forme::ERB::Form.form(obj, attr, opts, &block)
54
+ _forme_form_options(options)
55
+ _forme_form_class.form(obj, attr, opts, &block)
56
+ end
57
+
58
+ private
59
+
60
+ # The class to use for forms
61
+ def _forme_form_class
62
+ ::Forme::ERB::Form
63
+ end
64
+
65
+ # The options to use for forms. Any changes should mutate this hash to set options.
66
+ def _forme_form_options(options)
67
+ options
54
68
  end
55
69
  end
56
70
  end
@@ -0,0 +1,214 @@
1
+ # frozen-string-literal: true
2
+
3
+ require 'rack/utils'
4
+ require 'forme/erb_form'
5
+
6
+ class Roda
7
+ module RodaPlugins
8
+ module FormeSet
9
+ # Require the forme_route_csrf plugin.
10
+ def self.load_dependencies(app, _ = nil)
11
+ app.plugin :forme_route_csrf
12
+ end
13
+
14
+ # Set the HMAC secret.
15
+ def self.configure(app, opts = OPTS, &block)
16
+ app.opts[:forme_set_hmac_secret] = opts[:secret] || app.opts[:forme_set_hmac_secret]
17
+
18
+ if block
19
+ app.send(:define_method, :_forme_set_handle_error, &block)
20
+ app.send(:private, :_forme_set_handle_error)
21
+ end
22
+ end
23
+
24
+ # Error class raised for invalid form submissions.
25
+ class Error < StandardError
26
+ end
27
+
28
+ # Map of error types to error messages
29
+ ERROR_MESSAGES = {
30
+ :missing_data=>"_forme_set_data parameter not submitted",
31
+ :missing_hmac=>"_forme_set_data_hmac parameter not submitted",
32
+ :hmac_mismatch=>"_forme_set_data_hmac does not match _forme_set_data",
33
+ :csrf_mismatch=>"_forme_set_data CSRF token does not match submitted CSRF token",
34
+ :missing_namespace=>"no content in expected namespace"
35
+ }.freeze
36
+
37
+ # Forme::Form subclass that adds hidden fields with metadata that can be used
38
+ # to automatically process form submissions.
39
+ class Form < ::Forme::ERB::Form
40
+ def initialize(obj, opts=nil)
41
+ super
42
+ @forme_namespaces = @opts[:namespace]
43
+ end
44
+
45
+ # Try adding hidden fields to all forms
46
+ def form(*)
47
+ if block_given?
48
+ super do |f|
49
+ yield f
50
+ hmac_hidden_fields
51
+ end
52
+ else
53
+ t = super
54
+ if tags = hmac_hidden_fields
55
+ tags.each{|tag| t << tag}
56
+ end
57
+ t
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ # Add hidden fields with metadata, if the form has an object associated that
64
+ # supports the forme_inputs method, and it includes inputs.
65
+ def hmac_hidden_fields
66
+ if (obj = @opts[:obj]) && obj.respond_to?(:forme_inputs) && (forme_inputs = obj.forme_inputs)
67
+ columns = []
68
+ valid_values = {}
69
+
70
+ forme_inputs.each do |field, input|
71
+ next unless col = obj.send(:forme_column_for_input, input)
72
+ col = col.to_s
73
+ columns << col
74
+
75
+ next unless validation = obj.send(:forme_validation_for_input, field, input)
76
+ validation[0] = validation[0].to_s
77
+ has_nil = false
78
+ validation[1] = validation[1].map do |v|
79
+ has_nil ||= v.nil?
80
+ v.to_s
81
+ end
82
+ validation[1] << nil if has_nil
83
+ valid_values[col] = validation
84
+ end
85
+
86
+ return if columns.empty?
87
+
88
+ data = {}
89
+ data['columns'] = columns
90
+ data['namespaces'] = @forme_namespaces
91
+ data['csrf'] = @opts[:csrf]
92
+ data['valid_values'] = valid_values unless valid_values.empty?
93
+ data['form_version'] = @opts[:form_version] if @opts[:form_version]
94
+
95
+ data = data.to_json
96
+ tags = []
97
+ tags << tag(:input, :type=>:hidden, :name=>:_forme_set_data, :value=>data)
98
+ tags << tag(:input, :type=>:hidden, :name=>:_forme_set_data_hmac, :value=>OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, @opts[:roda].class.opts[:forme_set_hmac_secret], data))
99
+ tags.each{|tag| emit(tag)}
100
+ tags
101
+ end
102
+ end
103
+ end
104
+
105
+ module InstanceMethods
106
+ # Return hash based on submitted parameters, with :values key
107
+ # being submitted values for the object, and :validations key
108
+ # being a hash of validation metadata for the object.
109
+ def forme_parse(obj)
110
+ h = _forme_parse(obj)
111
+
112
+ params = h.delete(:params)
113
+ columns = h.delete(:columns)
114
+ h[:validations] ||= {}
115
+
116
+ values = h[:values] = {}
117
+ columns.each do |col|
118
+ values[col.to_sym] = params[col]
119
+ end
120
+
121
+ h
122
+ end
123
+
124
+ # Set fields on the object based on submitted parameters, as
125
+ # well as validations for associated object values.
126
+ def forme_set(obj)
127
+ h = _forme_parse(obj)
128
+
129
+ obj.set_fields(h[:params], h[:columns])
130
+
131
+ if h[:validations]
132
+ obj.forme_validations.merge!(h[:validations])
133
+ end
134
+
135
+ if block_given?
136
+ yield h[:form_version], obj
137
+ end
138
+
139
+ obj
140
+ end
141
+
142
+ private
143
+
144
+ # Raise error with message based on type
145
+ def _forme_set_handle_error(type, _obj)
146
+ end
147
+
148
+ # Raise error with message based on type
149
+ def _forme_parse_error(type, obj)
150
+ _forme_set_handle_error(type, obj)
151
+ raise Error, ERROR_MESSAGES[type]
152
+ end
153
+
154
+ # Use form class that adds hidden fields for metadata.
155
+ def _forme_form_class
156
+ Form
157
+ end
158
+
159
+ # Include a reference to the current scope to the form. This reference is needed
160
+ # to correctly construct the HMAC.
161
+ def _forme_form_options(options)
162
+ options.merge!(:roda=>self)
163
+ end
164
+
165
+ # Internals of forme_parse_hmac and forme_set_hmac.
166
+ def _forme_parse(obj)
167
+ params = request.params
168
+ return _forme_parse_error(:missing_data, obj) unless data = params['_forme_set_data']
169
+ return _forme_parse_error(:missing_hmac, obj) unless hmac = params['_forme_set_data_hmac']
170
+
171
+ data = data.to_s
172
+ hmac = hmac.to_s
173
+ actual = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, self.class.opts[:forme_set_hmac_secret], data)
174
+ unless Rack::Utils.secure_compare(hmac.ljust(64), actual) && hmac.length == actual.length
175
+ return _forme_parse_error(:hmac_mismatch, obj)
176
+ end
177
+
178
+ data = JSON.parse(data)
179
+ csrf_field, hmac_csrf_value = data['csrf']
180
+ if csrf_field
181
+ csrf_value = params[csrf_field].to_s
182
+ hmac_csrf_value = hmac_csrf_value.to_s
183
+ unless Rack::Utils.secure_compare(csrf_value.ljust(hmac_csrf_value.length), hmac_csrf_value) && csrf_value.length == hmac_csrf_value.length
184
+ return _forme_parse_error(:csrf_mismatch, obj)
185
+ end
186
+ end
187
+
188
+ namespaces = data['namespaces']
189
+ namespaces.each do |key|
190
+ return _forme_parse_error(:missing_namespace, obj) unless params = params[key]
191
+ end
192
+
193
+ if valid_values = data['valid_values']
194
+ validations = {}
195
+ valid_values.each do |col, (type, values)|
196
+ value = params[col]
197
+ valid = if type == "subset"
198
+ !value || (value - values).empty?
199
+ else # type == "include"
200
+ values.include?(value)
201
+ end
202
+
203
+ validations[col.to_sym] = [:valid, valid]
204
+ end
205
+ end
206
+
207
+ {:params=>params, :columns=>data["columns"], :validations=>validations, :form_version=>data['form_version']}
208
+ end
209
+ end
210
+ end
211
+
212
+ register_plugin(:forme_set, FormeSet)
213
+ end
214
+ end
@@ -192,6 +192,7 @@ module Sequel # :nodoc:
192
192
 
193
193
  # Set the error option correctly if the field contains errors
194
194
  def handle_errors(f)
195
+ return if opts.has_key?(:error)
195
196
  if e = obj.errors.on(f)
196
197
  opts[:error] = e.join(', ')
197
198
  end
@@ -207,6 +208,7 @@ module Sequel # :nodoc:
207
208
  # Update the attributes and options for any recognized validations
208
209
  def handle_validations(f)
209
210
  m = obj.model
211
+
210
212
  if m.respond_to?(:validation_reflections) and (vs = m.validation_reflections[f])
211
213
  attr = opts[:attr]
212
214
  vs.each do |type, options|
@@ -218,7 +220,7 @@ module Sequel # :nodoc:
218
220
  attr[:title] = options[:title] unless attr.has_key?(:title)
219
221
  when :length
220
222
  unless attr.has_key?(:maxlength)
221
- if max =(options[:maximum] || options[:is])
223
+ if max = (options[:maximum] || options[:is])
222
224
  attr[:maxlength] = max
223
225
  elsif (w = options[:within]) && w.is_a?(Range)
224
226
  attr[:maxlength] = if w.exclude_end? && w.end.is_a?(Integer)
@@ -345,7 +347,7 @@ module Sequel # :nodoc:
345
347
 
346
348
  # Delegate to the +form+.
347
349
  def humanize(s)
348
- form.humanize(s)
350
+ form.respond_to?(:humanize) ? form.humanize(s) : s.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize
349
351
  end
350
352
 
351
353
  # If the column allows +NULL+ values, use a three-valued select
@@ -442,6 +444,9 @@ module Sequel # :nodoc:
442
444
  # is overridden.
443
445
  def standard_input(type)
444
446
  type = opts.delete(:type) || type
447
+ if type.to_s =~ /\A(text|textarea|password|email|tel|url)\z/ && !opts[:attr].has_key?(:maxlength) && (sch = obj.db_schema[field]) && (max_length = sch[:max_length])
448
+ opts[:attr][:maxlength] = max_length
449
+ end
445
450
  opts[:value] = obj.send(field) unless opts.has_key?(:value)
446
451
  _input(type, opts)
447
452
  end
@@ -466,10 +471,10 @@ module Sequel # :nodoc:
466
471
  include SequelForm
467
472
  end
468
473
 
469
- module InstanceMethods
470
- MUTEX = Mutex.new
471
- FORM_CLASSES = {::Forme::Form=>Form}
474
+ MUTEX = Mutex.new
475
+ FORM_CLASSES = {::Forme::Form=>Form}
472
476
 
477
+ module InstanceMethods
473
478
  # Configure the +form+ with support for <tt>Sequel::Model</tt>
474
479
  # specific code, such as support for nested attributes.
475
480
  def forme_config(form)
@@ -3,7 +3,7 @@
3
3
  module Sequel # :nodoc:
4
4
  module Plugins # :nodoc:
5
5
  # The forme_set plugin makes the model instance keep track of which form
6
- # inputs have been added for it. It adds a forme_set method to handle
6
+ # inputs have been added for it. It adds a <tt>forme_set(params['model_name'])</tt> method to handle
7
7
  # the intake of submitted data from the form. For more complete control,
8
8
  # it also adds a forme_parse method that returns a hash of information that can be
9
9
  # used to modify and validate the object.
@@ -18,6 +18,7 @@ module Sequel # :nodoc:
18
18
  module InstanceMethods
19
19
  # Hash with column name symbol keys and Forme::SequelInput values
20
20
  def forme_inputs
21
+ return (@forme_inputs || {}) if frozen?
21
22
  @forme_inputs ||= {}
22
23
  end
23
24
 
@@ -25,12 +26,13 @@ module Sequel # :nodoc:
25
26
  # is a boolean flag, if true, the uploaded values should be a subset of the allowed values,
26
27
  # otherwise, there should be a single uploaded value that is a member of the allowed values.
27
28
  def forme_validations
29
+ return (@forme_validations || {}) if frozen?
28
30
  @forme_validations ||= {}
29
31
  end
30
32
 
31
33
  # Keep track of the inputs used.
32
34
  def forme_input(_form, field, _opts)
33
- forme_inputs[field] = super
35
+ frozen? ? super : (forme_inputs[field] = super)
34
36
  end
35
37
 
36
38
  # Given the hash of submitted parameters, return a hash containing information on how to
@@ -47,34 +49,11 @@ module Sequel # :nodoc:
47
49
  validations = hash[:validations] = {}
48
50
 
49
51
  forme_inputs.each do |field, input|
50
- opts = input.opts
51
- next if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form.opts[:formatter]})
52
-
53
- if attr = opts[:attr]
54
- name = attr[:name] || attr['name']
55
- end
56
- name ||= opts[:name] || opts[:key] || next
57
-
58
- # Pull out last component of the name if there is one
59
- column = (name =~ /\[([^\[\]]+)\]\z/ ? $1 : name)
60
- column = column.to_s.sub(/\[\]\z/, '').to_sym
61
-
52
+ next unless column = forme_column_for_input(input)
62
53
  hash_values[column] = params[column] || params[column.to_s]
63
54
 
64
- next unless ref = model.association_reflection(field)
65
- next unless options = opts[:options]
66
-
67
- values = if opts[:text_method]
68
- value_method = opts[:value_method] || opts[:text_method]
69
- options.map(&value_method)
70
- else
71
- options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
72
- end
73
-
74
- if ref[:type] == :many_to_one && !opts[:required]
75
- values << nil
76
- end
77
- validations[column] = [ref[:type] != :many_to_one ? :subset : :include, values]
55
+ next unless validation = forme_validation_for_input(field, input)
56
+ validations[column] = validation
78
57
  end
79
58
 
80
59
  hash
@@ -88,6 +67,7 @@ module Sequel # :nodoc:
88
67
  unless hash[:validations].empty?
89
68
  forme_validations.merge!(hash[:validations])
90
69
  end
70
+ nil
91
71
  end
92
72
 
93
73
  # Check associated values to ensure they match one of options in the form.
@@ -105,6 +85,8 @@ module Sequel # :nodoc:
105
85
  !value || (value - values).empty?
106
86
  when :include
107
87
  values.include?(value)
88
+ when :valid
89
+ values
108
90
  else
109
91
  raise Forme::Error, "invalid type used in forme_validations"
110
92
  end
@@ -115,6 +97,46 @@ module Sequel # :nodoc:
115
97
  end
116
98
  end
117
99
  end
100
+
101
+ private
102
+
103
+ # Return the model column name to use for the given form input.
104
+ def forme_column_for_input(input)
105
+ opts = input.opts
106
+ return if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form_opts[:formatter]})
107
+
108
+ if attr = opts[:attr]
109
+ name = attr[:name] || attr['name']
110
+ end
111
+ return unless name ||= opts[:name] || opts[:key]
112
+
113
+ # Pull out last component of the name if there is one
114
+ column = name.to_s.chomp('[]')
115
+ if column =~ /\[([^\[\]]+)\]\z/
116
+ $1
117
+ else
118
+ column
119
+ end.to_sym
120
+ end
121
+
122
+ # Return the validation metadata to use for the given field name and form input.
123
+ def forme_validation_for_input(field, input)
124
+ return unless ref = model.association_reflection(field)
125
+ opts = input.opts
126
+ return unless options = opts[:options]
127
+
128
+ values = if opts[:text_method]
129
+ value_method = opts[:value_method] || opts[:text_method]
130
+ options.map(&value_method)
131
+ else
132
+ options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
133
+ end
134
+
135
+ if ref[:type] == :many_to_one && !opts[:required]
136
+ values << nil
137
+ end
138
+ [ref[:type] != :many_to_one ? :subset : :include, values]
139
+ end
118
140
  end
119
141
  end
120
142
  end