forme 1.10.0 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 581902a62cc974ce05fecf52a4b7debd41b5d89f5ba151c1c03a2f0589f15e7e
4
- data.tar.gz: 5252e5dca95690ddf46341e443954058644f41c67e2109d7692a6dfb8c3b3ee8
3
+ metadata.gz: ee6b65bc378fc1ea557f3280b5782527735c561e58344e20920a1e728ce4592f
4
+ data.tar.gz: 4e2b963f58361fb962034c1727f122f22414c7bfcac8f80a969dfa216187ebc3
5
5
  SHA512:
6
- metadata.gz: 39c198d2db64a06eacf30e1ed2e5af10bce818fe45f81c17ab8fcd4db7b1693afb3c2e82d013615105dd71907e96eead22104340068b04361492b853fe03da65
7
- data.tar.gz: 33bfcf058c9ffc34f9581f3aa78e924b6764b034c56495ebaebd241eb07f5984e4330ddbab3f3a934cc77e66412e01c07decbf81bee07bb10c5d829eb00dff6f
6
+ metadata.gz: e40c6db1223714f80e6d603dcd25c48b3baa6ecc89126738f495b9ffc68b84412697b1874c248d8c87c94916b6be6325f47604be17da9c8be390fcced572a1ab
7
+ data.tar.gz: 5a5ed453fee72ac73348c5fed4083404758d2dbe979d0ca28fb72a50c125e167804e9e55b5ca1491f3e79bc860991e98f5926b35be82e47240763ed7a47a7ef9
data/CHANGELOG CHANGED
@@ -1,3 +1,7 @@
1
+ === 1.11.0 (2020-01-03)
2
+
3
+ * Add Roda forme_set plugin, using HMACed form metadata to automatically handle submitted form parameters (jeremyevans)
4
+
1
5
  === 1.10.0 (2019-05-13)
2
6
 
3
7
  * Make readonly formatter ignore hidden inputs (jeremyevans)
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011-2018 Jeremy Evans
1
+ Copyright (c) 2011-2019 Jeremy Evans
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to
@@ -589,7 +589,7 @@ As you can see, you basically need to recreate the conditionals used when creati
589
589
  the form, so that that the processing of the form submission handles only the
590
590
  inputs that were displayed on the form.
591
591
 
592
- === forme_set plugin
592
+ === forme_set Sequel plugin
593
593
 
594
594
  The forme_set plugin is designed to make handling form submissions easier. What it does
595
595
  is record the form fields that are used on the object, and then it uses those fields
@@ -686,6 +686,166 @@ internally). forme_parse returns a hash with the following keys:
686
686
  It is possible to use forme_set for the values it can handle, and set other fields manually
687
687
  using set_fields.
688
688
 
689
+ === forme_set Roda plugin
690
+
691
+ The forme_set Roda plugin builds on the forme_set Sequel plugin and is designed to make
692
+ handling form submissions even easier. This plugin uses a hidden form input to store which
693
+ fields were used to build the form, as well as some other metadata. It uses another hidden
694
+ form input with an HMAC, so that on submission, if the HMAC matches, you can be sure that an
695
+ attacker didn't add extra fields.
696
+
697
+ There are a couple advantages to this plugin over using just the Sequel forme_set plugin.
698
+ One is that you do not need to record the form fields when processing the submission of a
699
+ form, since the information you need is included in the form submission. Another is that
700
+ calling the forme_set method is simpler, since it can determine the necessary parameters.
701
+
702
+ While you need code like this when using just the Sequel forme_set plugin:
703
+
704
+ album = Album[1]
705
+ Forme.form(album, :action=>'/foo') do |f|
706
+ f.input :name
707
+ f.input :copies_sold if album.released?
708
+ end
709
+ album.forme_set(params['album'])
710
+
711
+ when you also use the Roda forme_set plugin, you can simplify it to:
712
+
713
+ album = Album[1]
714
+ forme_set(album)
715
+
716
+ ==== Validations
717
+
718
+ The Roda forme_set plugin supports and uses the same validations as the Sequel forme_set
719
+ plugin. However, the Roda plugin is more accurate because it uses the options that were
720
+ present on the form when it was originally built, instead of the options that would be
721
+ present on the form when the form was submitted. However, note that that can be a
722
+ negative if you are dynamically adding values to both the database and the form between
723
+ when the form was built and when it was submitted.
724
+
725
+ ==== Usage
726
+
727
+ Because the Roda forme_set plugin includes the metadata needed to process the form in form
728
+ submissions, you don't need to rearrange code to use it, or rerender templates.
729
+ You can do:
730
+
731
+ album = Album[1]
732
+ forme_set(album)
733
+
734
+ And the method will update the +album+ object using the appropriate form values.
735
+
736
+ Note that using the Roda forme_set plugin requires you set a secret for the HMAC. It
737
+ is important that you keep this value secret, because if an attacker has access to this,
738
+ they would be able to set arbitrary attributes for model objects. In your Roda class,
739
+ you can load the plugin via:
740
+
741
+ plugin :forme_set, :secret => ENV["APP_FORME_HMAC_SECRET"]
742
+
743
+ By default, invalid form submissions will raise an exception. If you want to change
744
+ that behavior (i.e. to display a nice error page), pass a block when loading the plugin:
745
+
746
+ plugin :forme_set do |error_type, obj|
747
+ # ...
748
+ end
749
+
750
+ The block arguments will be a symbol for the type of error (:missing_data, :missing_hmac,
751
+ :hmac_mismatch, :csrf_mismatch, or :missing_namespace) and the object passed to +forme_set+.
752
+ This block should raise or halt. If it does not, the default behavior of raising an
753
+ exception will be taken.
754
+
755
+ === Form Versions
756
+
757
+ The Roda forme_set plugin supports form versions. This allows you to gracefully handle
758
+ changes to forms, processing submissions of the form generated before the change (if
759
+ possible) as well as the processing submissions of the form generated after the change.
760
+
761
+ For example, maybe you have an existing form with just an input for the name:
762
+
763
+ form(album) do |f|
764
+ f.input(:name)
765
+ end
766
+
767
+ Then later, you want to add an input for the number of copies sold:
768
+
769
+ form(album) do |f|
770
+ f.input(:name)
771
+ f.input(:copies_sold)
772
+ end
773
+
774
+ Using the Roda forme_set plugin, submissions of the old form would only set the
775
+ name field, it wouldn't set the copies_sold field, since when the form was created,
776
+ only the name field was used.
777
+
778
+ You can handle this case be versioning the form when making changes to it:
779
+
780
+ form(album, {}, :form_version=>1) do |f|
781
+ f.input(:name)
782
+ f.input(:copies_sold)
783
+ end
784
+
785
+ When you are processing the form submission with forme_set, you pass a block, which
786
+ will be yielded the version for the form (nil if no version was set):
787
+
788
+ forme_set(album) do |version|
789
+ if version == nil
790
+ album.copies_sold = 0
791
+ end
792
+ end
793
+
794
+ The block is also yielded the object passed for forme_set, useful if you don't keep
795
+ a reference to it:
796
+
797
+ album = forme_set(Album.new) do |version, obj|
798
+ if version == nil
799
+ obj.copies_sold = 0
800
+ end
801
+ end
802
+
803
+ You only need to support old versions of the form for as long as their could be
804
+ active sessions that could use the old versions of the form. As long you as
805
+ are expiring sessions to prevent session fixation, you can remove the version
806
+ handling after the expiration period has passed since the change to the form
807
+ was made.
808
+
809
+ Note that this issue with handling changes to forms is not specific to the Roda
810
+ forme_set plugin, it affects pretty much all form submissions. The Roda forme_set
811
+ plugin just makes this issue easier to handle.
812
+
813
+ ==== Caveats
814
+
815
+ The Roda forme_set plugin has basically the same caveats as Sequel forme_set plugin.
816
+ Additionally, it has a couple other restrictions that the Sequel forme_set plugin
817
+ does not have.
818
+
819
+ First, the Roda forme_set plugin only handles a single object in forms,
820
+ which must be provided when creating the form. It does not handle multiple
821
+ objects in the same form, and ignores any fields set for an object different
822
+ from the one passed when creating the form. You can use the Sequel forme_set
823
+ plugin to handle form submissions involving multiple objects, or for the
824
+ objects that were not passed when creating the form.
825
+
826
+ Second, the Roda forme_set plugin does not handle cases where the field values
827
+ are placed outside the forms default namespace. The Sequel forme_set plugin
828
+ can handle those issues, as long as all values are in the same namespace, since
829
+ the Sequel forme_set plugin requires you pass in the specific hash to use (the
830
+ Roda forme_set plugin use the form's namespace information and the submitted
831
+ parameters to determine the hash to use).
832
+
833
+ In cases where the Roda forme_set does not handle things correctly, you can use
834
+ forme_parse, which will return metadata in the same format as the Sequel plugin
835
+ forme_parse method, with the addition of a :form_version key in the hash for the
836
+ form version.
837
+
838
+ It is possible to use the Roda forme_set plugin for the submissions it can handle, the
839
+ Sequel forme_set plugin for the submissions it can handle, and set other fields manually
840
+ using the Sequel set_fields methods.
841
+
842
+ Note that when using the Roda forme_set plugin with an existing form, you should first
843
+ enable the Roda plugin without actually using the Roda forme_set method. Do not
844
+ start using the Roda forme_set method until all currently valid sessions were
845
+ established after the Roda forme_set plugin was enabled. Otherwise, sessions that
846
+ access the form before the Roda forme_set plugin was enabled will not work if they
847
+ submit the form after the Roda forme_set plugin is enabled.
848
+
689
849
  == Other Sequel Plugins
690
850
 
691
851
  In addition to the Sequel plugins mentioned above, Forme also ships with additional Sequel
@@ -695,9 +855,9 @@ forme_i18n :: Handles translations for labels using i18n.
695
855
 
696
856
  = Roda Support
697
857
 
698
- Forme ships with two Roda plugins, forme and forme_route_csrf. For new code, it is
699
- recommended to use forme_route_csrf, as that uses Roda's route_csrf plugin, which
700
- supports more secure request-specific CSRF tokens. In both cases, usage in ERB
858
+ Forme ships with three Roda plugins: forme_set (discussed above), forme, and forme_route_csrf.
859
+ For new code, it is recommended to use forme_route_csrf, as that uses Roda's route_csrf
860
+ plugin, which supports more secure request-specific CSRF tokens. In both cases, usage in ERB
701
861
  templates is the same:
702
862
 
703
863
  <% form(@obj, :action=>'/foo') do |f| %>
@@ -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 = 10
9
+ MINOR = 11
10
10
 
11
11
  # The patch version of Forme, updated only for bug fixes from the last
12
12
  # feature release.
@@ -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
@@ -470,10 +470,10 @@ module Sequel # :nodoc:
470
470
  include SequelForm
471
471
  end
472
472
 
473
- module InstanceMethods
474
- MUTEX = Mutex.new
475
- FORM_CLASSES = {::Forme::Form=>Form}
473
+ MUTEX = Mutex.new
474
+ FORM_CLASSES = {::Forme::Form=>Form}
476
475
 
476
+ module InstanceMethods
477
477
  # Configure the +form+ with support for <tt>Sequel::Model</tt>
478
478
  # specific code, such as support for nested attributes.
479
479
  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.
@@ -47,34 +47,11 @@ module Sequel # :nodoc:
47
47
  validations = hash[:validations] = {}
48
48
 
49
49
  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
-
50
+ next unless column = forme_column_for_input(input)
62
51
  hash_values[column] = params[column] || params[column.to_s]
63
52
 
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]
53
+ next unless validation = forme_validation_for_input(field, input)
54
+ validations[column] = validation
78
55
  end
79
56
 
80
57
  hash
@@ -88,6 +65,7 @@ module Sequel # :nodoc:
88
65
  unless hash[:validations].empty?
89
66
  forme_validations.merge!(hash[:validations])
90
67
  end
68
+ nil
91
69
  end
92
70
 
93
71
  # Check associated values to ensure they match one of options in the form.
@@ -105,6 +83,8 @@ module Sequel # :nodoc:
105
83
  !value || (value - values).empty?
106
84
  when :include
107
85
  values.include?(value)
86
+ when :valid
87
+ values
108
88
  else
109
89
  raise Forme::Error, "invalid type used in forme_validations"
110
90
  end
@@ -115,6 +95,46 @@ module Sequel # :nodoc:
115
95
  end
116
96
  end
117
97
  end
98
+
99
+ private
100
+
101
+ # Return the model column name to use for the given form input.
102
+ def forme_column_for_input(input)
103
+ opts = input.opts
104
+ return if SKIP_FORMATTERS.include?(opts.fetch(:formatter){input.form_opts[:formatter]})
105
+
106
+ if attr = opts[:attr]
107
+ name = attr[:name] || attr['name']
108
+ end
109
+ return unless name ||= opts[:name] || opts[:key]
110
+
111
+ # Pull out last component of the name if there is one
112
+ column = name.to_s.chomp('[]')
113
+ if column =~ /\[([^\[\]]+)\]\z/
114
+ $1
115
+ else
116
+ column
117
+ end.to_sym
118
+ end
119
+
120
+ # Return the validation metadata to use for the given field name and form input.
121
+ def forme_validation_for_input(field, input)
122
+ return unless ref = model.association_reflection(field)
123
+ opts = input.opts
124
+ return unless options = opts[:options]
125
+
126
+ values = if opts[:text_method]
127
+ value_method = opts[:value_method] || opts[:text_method]
128
+ options.map(&value_method)
129
+ else
130
+ options.map{|obj| obj.is_a?(Array) ? obj.last : obj}
131
+ end
132
+
133
+ if ref[:type] == :many_to_one && !opts[:required]
134
+ values << nil
135
+ end
136
+ [ref[:type] != :many_to_one ? :subset : :include, values]
137
+ end
118
138
  end
119
139
  end
120
140
  end
@@ -20,6 +20,7 @@ class FormeRails < Rails::Application
20
20
  end
21
21
  end
22
22
  config.active_support.deprecation = :stderr
23
+ config.middleware.delete(ActionDispatch::HostAuthorization) if defined?(ActionDispatch::HostAuthorization)
23
24
  config.middleware.delete(ActionDispatch::ShowExceptions)
24
25
  config.middleware.delete(Rack::Lock)
25
26
  config.secret_key_base = 'foo'*15
@@ -122,6 +122,354 @@ else
122
122
  sin_get('/csrf/0').wont_include '<input name="_csrf" type="hidden" value="'
123
123
  end
124
124
  end
125
+
126
+ describe "Forme Roda ERB Sequel integration with roda forme_set plugin and route_csrf plugin with #{plugin_opts}" do
127
+ before do
128
+ @app = Class.new(FormeRodaTest)
129
+ @app.plugin :route_csrf, plugin_opts
130
+ @app.plugin(:forme_set, :secret=>'1'*64)
131
+
132
+ @ab = Album.new
133
+ end
134
+
135
+ def forme_parse(*args, &block)
136
+ _forme_set(:forme_parse, *args, &block)
137
+ end
138
+
139
+ def forme_set(*args, &block)
140
+ _forme_set(:forme_set, *args, &block)
141
+ end
142
+
143
+ def forme_call(params)
144
+ @app.call('REQUEST_METHOD'=>'POST', 'rack.input'=>StringIO.new, :params=>params)
145
+ end
146
+
147
+ def _forme_set(meth, obj, orig_hash, *form_args, &block)
148
+ hash = {}
149
+ forme_set_block = orig_hash.delete(:forme_set_block)
150
+ orig_hash.each{|k,v| hash[k.to_s] = v}
151
+ album = @ab
152
+ ret, form, data, hmac = nil
153
+
154
+ @app.route do |r|
155
+ r.get do
156
+ form(*env[:args], &env[:block]).to_s
157
+ end
158
+ r.post do
159
+ r.params.replace(env[:params])
160
+ ret = send(meth, album, &forme_set_block)
161
+ nil
162
+ end
163
+ end
164
+ body = @app.call('REQUEST_METHOD'=>'GET', :args=>[album, *form_args], :block=>block)[2].join
165
+ body =~ %r|<input name="_csrf" type="hidden" value="([^"]+)"/>.*<input name="_forme_set_data" type="hidden" value="([^"]+)"/><input name="_forme_set_data_hmac" type="hidden" value="([^"]+)"/>|n
166
+ csrf = $1
167
+ data = $2
168
+ hmac = $3
169
+ data.gsub!("&quot;", '"') if data
170
+ h = {"album"=>hash, "_forme_set_data"=>data, "_forme_set_data_hmac"=>hmac, "_csrf"=>csrf}
171
+ if data && hmac
172
+ forme_call(h)
173
+ end
174
+ meth == :forme_parse ? ret : h
175
+ end
176
+
177
+ it "#forme_set should include HMAC values if form includes inputs for obj" do
178
+ h = forme_set(@ab, :name=>'Foo')
179
+ proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
180
+ @ab.name.must_be_nil
181
+ @ab.copies_sold.must_be_nil
182
+
183
+ h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
184
+ hmac = h.delete("_forme_set_data_hmac")
185
+ proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
186
+ proc{forme_call(h.merge("_forme_set_data_hmac"=>hmac+'1'))}.must_raise Roda::RodaPlugins::FormeSet::Error
187
+ data = h["_forme_set_data"]
188
+ data.sub!(/"csrf":\["_csrf","./, "\"csrf\":[\"_csrf\",\"|")
189
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, '1'*64, data)
190
+ proc{forme_call(h.merge("_forme_set_data_hmac"=>hmac))}.must_raise Roda::RodaPlugins::FormeSet::Error
191
+ @ab.name.must_equal 'Foo'
192
+ @ab.copies_sold.must_be_nil
193
+
194
+ forme_set(@ab, :copies_sold=>100){|f| f.input(:name)}
195
+ @ab.name.must_be_nil
196
+ @ab.copies_sold.must_be_nil
197
+ end
198
+
199
+ it "#forme_set should handle custom form namespaces" do
200
+ forme_set(@ab, {"album"=>{"name"=>'Foo', 'copies_sold'=>'100'}}, {}, :namespace=>'album'){|f| f.input(:name); f.input(:copies_sold)}
201
+ @ab.name.must_equal 'Foo'
202
+ @ab.copies_sold.must_equal 100
203
+
204
+ proc{forme_set(@ab, {"a"=>{"name"=>'Foo'}}, {}, :namespace=>'album'){|f| f.input(:name); f.input(:copies_sold)}}.must_raise Roda::RodaPlugins::FormeSet::Error
205
+ end
206
+
207
+ it "#forme_set should call plugin block if there is an error with the form submission hmac not matching data" do
208
+ @app.plugin :forme_set do |error_type, _|
209
+ request.on{error_type.to_s}
210
+ end
211
+
212
+ h = forme_set(@ab, :name=>'Foo')
213
+ forme_call(h)[2].must_equal ['missing_data']
214
+
215
+ h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
216
+ hmac = h.delete("_forme_set_data_hmac")
217
+ forme_call(h)[2].must_equal ['missing_hmac']
218
+
219
+ forme_call(h.merge("_forme_set_data_hmac"=>hmac+'1'))[2].must_equal ['hmac_mismatch']
220
+
221
+ data = h["_forme_set_data"]
222
+ data.sub!(/"csrf":\["_csrf","./, "\"csrf\":[\"_csrf\",\"|")
223
+ hmac = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA512.new, '1'*64, data)
224
+ forme_call(h.merge("_forme_set_data_hmac"=>hmac))[2].must_equal ['csrf_mismatch']
225
+
226
+ h = forme_set(@ab, :name=>'Foo')
227
+ h.delete('album')
228
+ forme_call(h)[2].must_equal ['missing_namespace']
229
+ end
230
+
231
+ it "#forme_set should raise if plugin block does not raise or throw" do
232
+ @app.plugin :forme_set do |_, obj|
233
+ obj
234
+ end
235
+ h = forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
236
+ h.delete("_forme_set_data_hmac")
237
+ proc{forme_call(h)}.must_raise Roda::RodaPlugins::FormeSet::Error
238
+ end
239
+
240
+ it "#forme_set should only set values in the form" do
241
+ forme_set(@ab, :name=>'Foo')
242
+ @ab.name.must_be_nil
243
+
244
+ forme_set(@ab, :name=>'Foo'){|f| f.input(:name)}
245
+ @ab.name.must_equal 'Foo'
246
+
247
+ forme_set(@ab, 'copies_sold'=>'1'){|f| f.input(:name)}
248
+ @ab.name.must_be_nil
249
+ @ab.copies_sold.must_be_nil
250
+
251
+ forme_set(@ab, 'name'=>'Bar', 'copies_sold'=>'1'){|f| f.input(:name); f.input(:copies_sold)}
252
+ @ab.name.must_equal 'Bar'
253
+ @ab.copies_sold.must_equal 1
254
+ end
255
+
256
+ it "#forme_set should handle form_versions" do
257
+ h = forme_set(@ab, {:name=>'Foo'}){|f| f.input(:name)}
258
+ @ab.name.must_equal 'Foo'
259
+
260
+ obj = nil
261
+ version = nil
262
+ name = nil
263
+ forme_set_block = proc do |v, o|
264
+ obj = o
265
+ name = o.name
266
+ version = v
267
+ end
268
+ h2 = forme_set(@ab, {:name=>'Foo', :forme_set_block=>forme_set_block}, {}, :form_version=>1){|f| f.input(:name)}
269
+ obj.must_be_same_as @ab
270
+ name.must_equal 'Foo'
271
+ version.must_equal 1
272
+
273
+ forme_call(h)
274
+ obj.must_be_same_as @ab
275
+ version.must_be_nil
276
+
277
+ h3 = forme_set(@ab, {:name=>'Bar', :forme_set_block=>forme_set_block}, {}, :form_version=>2){|f| f.input(:name)}
278
+ obj.must_be_same_as @ab
279
+ name.must_equal 'Bar'
280
+ version.must_equal 2
281
+
282
+ h['album']['name'] = 'Baz'
283
+ forme_call(h)
284
+ obj.must_be_same_as @ab
285
+ name.must_equal 'Baz'
286
+ version.must_be_nil
287
+
288
+ forme_call(h2)
289
+ obj.must_be_same_as @ab
290
+ version.must_equal 1
291
+ end
292
+
293
+ it "#forme_set should work for forms without blocks" do
294
+ forme_set(@ab, {:name=>'Foo'}, {}, :inputs=>[:name])
295
+ @ab.name.must_equal 'Foo'
296
+ end
297
+
298
+ it "#forme_set should handle different ways to specify parameter names" do
299
+ [{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
300
+ forme_set(@ab, name=>'Foo'){|f| f.input(:name, opts)}
301
+ @ab.name.must_be_nil
302
+
303
+ forme_set(@ab, 'foo'=>'Foo'){|f| f.input(:name, opts)}
304
+ @ab.name.must_equal 'Foo'
305
+ end
306
+ end
307
+
308
+ it "#forme_set should ignore values where key is explicitly set to nil" do
309
+ forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :key=>nil)}
310
+ @ab.forme_set(:name=>'Foo')
311
+ @ab.name.must_be_nil
312
+ @ab.forme_set(nil=>'Foo')
313
+ @ab.name.must_be_nil
314
+ end
315
+
316
+ it "#forme_set should skip inputs with disabled/readonly formatter set on input" do
317
+ [:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
318
+ forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :formatter=>formatter)}
319
+ @ab.name.must_be_nil
320
+ end
321
+
322
+ forme_set(@ab, :name=>'Foo'){|f| f.input(:name, :formatter=>:default)}
323
+ @ab.name.must_equal 'Foo'
324
+ end
325
+
326
+ it "#forme_set should skip inputs with disabled/readonly formatter set on Form" do
327
+ [:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
328
+ forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:disabled){|f| f.input(:name)}
329
+ @ab.name.must_be_nil
330
+ end
331
+
332
+ forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:default){|f| f.input(:name)}
333
+ @ab.name.must_equal 'Foo'
334
+ end
335
+
336
+ it "#forme_set should skip inputs with disabled/readonly formatter set using with_opts" do
337
+ [:disabled, :readonly, ::Forme::Formatter::Disabled, ::Forme::Formatter::ReadOnly].each do |formatter|
338
+ forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>formatter){f.input(:name)}}
339
+ @ab.name.must_be_nil
340
+ end
341
+
342
+ forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:default){f.input(:name)}}
343
+ @ab.name.must_equal 'Foo'
344
+ end
345
+
346
+ it "#forme_set should prefer input formatter to with_opts formatter" do
347
+ forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:default){f.input(:name, :formatter=>:readonly)}}
348
+ @ab.name.must_be_nil
349
+
350
+ forme_set(@ab, :name=>'Foo'){|f| f.with_opts(:formatter=>:readonly){f.input(:name, :formatter=>:default)}}
351
+ @ab.name.must_equal 'Foo'
352
+ end
353
+
354
+ it "#forme_set should prefer with_opts formatter to form formatter" do
355
+ forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:default){|f| f.with_opts(:formatter=>:readonly){f.input(:name)}}
356
+ @ab.name.must_be_nil
357
+
358
+ forme_set(@ab, {:name=>'Foo'}, {}, :formatter=>:readonly){|f| f.with_opts(:formatter=>:default){f.input(:name)}}
359
+ @ab.name.must_equal 'Foo'
360
+ end
361
+
362
+ it "#forme_set should handle setting values for associated objects" do
363
+ forme_set(@ab, :artist_id=>'1')
364
+ @ab.artist_id.must_be_nil
365
+
366
+ forme_set(@ab, :artist_id=>'1'){|f| f.input(:artist)}
367
+ @ab.artist_id.must_equal 1
368
+
369
+ forme_set(@ab, 'tag_pks'=>%w'1 2'){|f| f.input(:artist)}
370
+ @ab.artist_id.must_be_nil
371
+ @ab.tag_pks.must_equal []
372
+
373
+ forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'1 2'){|f| f.input(:artist); f.input(:tags)}
374
+ @ab.artist_id.must_equal 1
375
+ @ab.tag_pks.must_equal [1, 2]
376
+ end
377
+
378
+ it "#forme_set should handle validations for filtered associations" do
379
+ [
380
+ [{:dataset=>proc{|ds| ds.exclude(:id=>1)}},
381
+ {:dataset=>proc{|ds| ds.exclude(:id=>1)}}],
382
+ [{:options=>Artist.exclude(:id=>1).select_order_map([:name, :id])},
383
+ {:options=>Tag.exclude(:id=>1).select_order_map(:id), :name=>'tag_pks[]'}],
384
+ [{:options=>Artist.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id},
385
+ {:options=>Tag.exclude(:id=>1).all, :text_method=>:name, :value_method=>:id}],
386
+ ].each do |artist_opts, tag_opts|
387
+ @ab.forme_validations.clear
388
+ forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'1 2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
389
+ @ab.artist_id.must_equal 1
390
+ @ab.tag_pks.must_equal [1, 2]
391
+ @ab.valid?.must_equal false
392
+ @ab.errors[:artist_id].must_equal ['invalid value submitted']
393
+ @ab.errors[:tag_pks].must_equal ['invalid value submitted']
394
+
395
+ @ab.forme_validations.clear
396
+ forme_set(@ab, 'artist_id'=>'1', 'tag_pks'=>%w'2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
397
+ @ab.forme_set('artist_id'=>'1', 'tag_pks'=>['2'])
398
+ @ab.artist_id.must_equal 1
399
+ @ab.tag_pks.must_equal [2]
400
+ @ab.valid?.must_equal false
401
+ @ab.errors[:artist_id].must_equal ['invalid value submitted']
402
+ @ab.errors[:tag_pks].must_be_nil
403
+
404
+ @ab.forme_validations.clear
405
+ forme_set(@ab, 'artist_id'=>'2', 'tag_pks'=>%w'2'){|f| f.input(:artist, artist_opts); f.input(:tags, tag_opts)}
406
+ @ab.valid?.must_equal true
407
+ end
408
+ end
409
+
410
+ it "#forme_set should not require associated values for many_to_one association with select boxes" do
411
+ forme_set(@ab, {}){|f| f.input(:artist)}
412
+ @ab.valid?.must_equal true
413
+
414
+ forme_set(@ab, {'artist_id'=>nil}){|f| f.input(:artist)}
415
+ @ab.valid?.must_equal true
416
+
417
+ forme_set(@ab, {'artist_id'=>''}){|f| f.input(:artist)}
418
+ @ab.valid?.must_equal true
419
+ end
420
+
421
+ it "#forme_set should not require associated values for many_to_one association with radio buttons" do
422
+ forme_set(@ab, {}){|f| f.input(:artist, :as=>:radio)}
423
+ @ab.valid?.must_equal true
424
+ end
425
+
426
+ it "#forme_set should require associated values for many_to_one association with select boxes when :required is used" do
427
+ forme_set(@ab, {}){|f| f.input(:artist, :required=>true)}
428
+ @ab.valid?.must_equal false
429
+ @ab.errors[:artist_id].must_equal ['invalid value submitted']
430
+ end
431
+
432
+ it "#forme_set should require associated values for many_to_one association with radio buttons when :required is used" do
433
+ forme_set(@ab, {}){|f| f.input(:artist, :as=>:radio, :required=>true)}
434
+ @ab.valid?.must_equal false
435
+ @ab.errors[:artist_id].must_equal ['invalid value submitted']
436
+ end
437
+
438
+ it "#forme_set should handle cases where currently associated values is nil" do
439
+ def @ab.tag_pks; nil; end
440
+ forme_set(@ab, :tag_pks=>['1']){|f| f.input(:tags)}
441
+ @ab.valid?.must_equal true
442
+ end
443
+
444
+ it "#forme_parse should return hash with values and validations" do
445
+ forme_parse(@ab, :name=>'Foo'){|f| f.input(:name)}.must_equal(:values=>{:name=>'Foo'}, :validations=>{}, :form_version=>nil)
446
+
447
+ hash = forme_parse(@ab, :name=>'Foo', 'artist_id'=>'1') do |f|
448
+ f.input(:name)
449
+ f.input(:artist, :dataset=>proc{|ds| ds.exclude(:id=>1)})
450
+ end
451
+ hash.must_equal(:values=>{:name=>'Foo', :artist_id=>'1'}, :validations=>{:artist_id=>[:valid, false]}, :form_version=>nil)
452
+
453
+ @ab.set(hash[:values])
454
+ @ab.valid?.must_equal true
455
+
456
+ @ab.forme_validations.merge!(hash[:validations])
457
+ @ab.valid?.must_equal false
458
+ @ab.errors[:artist_id].must_equal ['invalid value submitted']
459
+
460
+ @ab = Album.new
461
+ hash = forme_parse(@ab, {:name=>'Foo', 'artist_id'=>'1'}, {}, :form_version=>1) do |f|
462
+ f.input(:name)
463
+ f.input(:artist, :dataset=>proc{|ds| ds.exclude(:id=>2)})
464
+ end
465
+ hash.must_equal(:values=>{:name=>'Foo', :artist_id=>'1'}, :validations=>{:artist_id=>[:valid, true]}, :form_version=>1)
466
+ @ab.set(hash[:values])
467
+ @ab.valid?.must_equal true
468
+
469
+ @ab.forme_validations.merge!(hash[:validations])
470
+ @ab.valid?.must_equal true
471
+ end
472
+ end
125
473
  end
126
474
  end
127
475
  end
@@ -26,7 +26,7 @@ describe "Sequel forme_set plugin" do
26
26
  end
27
27
 
28
28
  it "#forme_set should handle different ways to specify parameter names" do
29
- [{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
29
+ [{:attr=>{:name=>'foo'}}, {:attr=>{'name'=>:foo}}, {:name=>'foo'}, {:name=>'foo[]'}, {:name=>'bar[foo][]'}, {:name=>'bar[foo]'}, {:key=>:foo}].each do |opts|
30
30
  @f.input(:name, opts)
31
31
 
32
32
  @ab.forme_set(:name=>'Foo')
@@ -15,4 +15,4 @@ require 'forme'
15
15
  require 'rubygems'
16
16
  ENV['MT_NO_PLUGINS'] = '1' # Work around stupid autoloading of plugins
17
17
  gem 'minitest'
18
- require 'minitest/autorun'
18
+ require 'minitest/global_expectations/autorun'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: forme
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-13 00:00:00.000000000 Z
11
+ date: 2020-01-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 5.7.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: minitest-global_expectations
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: sequel
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -67,7 +81,7 @@ dependencies:
67
81
  - !ruby/object:Gem::Version
68
82
  version: '0'
69
83
  - !ruby/object:Gem::Dependency
70
- name: erubis
84
+ name: erubi
71
85
  requirement: !ruby/object:Gem::Requirement
72
86
  requirements:
73
87
  - - ">="
@@ -175,6 +189,7 @@ files:
175
189
  - lib/forme/version.rb
176
190
  - lib/roda/plugins/forme.rb
177
191
  - lib/roda/plugins/forme_route_csrf.rb
192
+ - lib/roda/plugins/forme_set.rb
178
193
  - lib/sequel/plugins/forme.rb
179
194
  - lib/sequel/plugins/forme_i18n.rb
180
195
  - lib/sequel/plugins/forme_set.rb
@@ -197,7 +212,12 @@ files:
197
212
  homepage: http://github.com/jeremyevans/forme
198
213
  licenses:
199
214
  - MIT
200
- metadata: {}
215
+ metadata:
216
+ bug_tracker_uri: https://github.com/jeremyevans/forme/issues
217
+ changelog_uri: http://forme.jeremyevans.net/files/CHANGELOG.html
218
+ documentation_uri: http://forme.jeremyevans.net
219
+ mailing_list_uri: https://groups.google.com/forum/#!forum/ruby-forme
220
+ source_code_uri: https://github.com/jeremyevans/forme
201
221
  post_install_message:
202
222
  rdoc_options:
203
223
  - "--quiet"
@@ -220,7 +240,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
220
240
  - !ruby/object:Gem::Version
221
241
  version: '0'
222
242
  requirements: []
223
- rubygems_version: 3.0.3
243
+ rubygems_version: 3.1.2
224
244
  signing_key:
225
245
  specification_version: 4
226
246
  summary: HTML forms library