forme 1.8.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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