sinatra-accept-params 0.1.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.
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Nate Wiger
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,69 @@
1
+ Sinatra::AcceptParams - Parameter whitelisting for Sinatra
2
+ ==========================================================
3
+
4
+ This plugin adds parameter whitelisting, type checking, and validation at the routing level
5
+ to a Sinatra application. While model-level validations are good for CRUD operations, in many
6
+ cases there are other input parameters which are either not part or a model, or which you want to
7
+ verify before executing lots of (potentially unsafe) code just to have your model raise an
8
+ error. Examples include:
9
+
10
+ * page numbers for pagination
11
+ * search strings
12
+ * routing prefixes such as region or language
13
+
14
+ In addition, this plugin provides several extended capabilities which come in handy:
15
+
16
+ * type checking of parameters (eg, integers vs strings)
17
+ * automatic type casting of parameters (helps with plugins such as +will_paginate+)
18
+ * default values and post-processing of params
19
+
20
+ Example
21
+ =======
22
+
23
+ # GET /channels
24
+ # GET /channels.xml
25
+ def index
26
+ accept_params do |p|
27
+ p.integer :page, :default => 1, :minvalue => 1
28
+ p.integer :per_page, :default => 50, :minvalue => 1
29
+ end
30
+ end
31
+
32
+
33
+ # POST /rating
34
+ # POST /rating.xml
35
+ def create
36
+ accept_params do |p|
37
+ p.namespace :rating do |p|
38
+ p.integer :user_id, :required => true, :minvalue => 1
39
+ p.integer :rating, :required => true
40
+ p.string :comments, :process => Proc.new(value){ my_value_cleaner(value) }
41
+ end
42
+ end
43
+
44
+ @rating = Rating.new(params[:rating])
45
+ @rating.save
46
+
47
+ # format/response code
48
+ end
49
+
50
+
51
+ # GET /players/1
52
+ # GET /players/1.xml
53
+ def show
54
+ accept_only_id
55
+ @player = Player.find(params[:id])
56
+
57
+ respond_to do |format|
58
+ format.html # show.html.erb
59
+ format.xml { render :xml => @player }
60
+ end
61
+ end
62
+
63
+
64
+ Author
65
+ ======
66
+
67
+ Copyright (c) 2008-2010 {Nate Wiger}[http://nateware.com]. All Rights Reserved.
68
+ This code is released under the Artistic License.
69
+
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "sinatra-accept-params"
8
+ gem.summary = %Q{Parameter whitelisting for Sinatra}
9
+ gem.description = %Q{Parameter whitelisting for Sinatra. Provides validation, defaults, and post-processing.}
10
+ gem.email = "nate@wiger.org"
11
+ gem.homepage = "http://github.com/nateware/sinatra-accept-params"
12
+ gem.authors = ["Nate Wiger"]
13
+ gem.add_development_dependency "bacon", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:spec) do |spec|
23
+ spec.libs << 'lib' << 'spec'
24
+ spec.pattern = 'spec/**/*_spec.rb'
25
+ spec.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |spec|
31
+ spec.libs << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :spec => :check_dependencies
42
+
43
+ task :default => :spec
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "sinatra-accept-params #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,95 @@
1
+
2
+ module Sinatra
3
+ module AcceptParams
4
+ # Exceptions for AcceptParams
5
+ class ParamError < StandardError; end #:nodoc:
6
+ class NoParamsDefined < ParamError; end #:nodoc:
7
+ class MissingParam < ParamError; end #:nodoc:
8
+ class UnexpectedParam < ParamError; end #:nodoc:
9
+ class InvalidParamType < ParamError; end #:nodoc:
10
+ class InvalidParamValue < ParamError; end #:nodoc:
11
+ class SslRequired < ParamError; end #:nodoc:
12
+ class LoginRequired < ParamError; end #:nodoc:
13
+
14
+ # Below here are settings that can be modified in environment.rb
15
+ # Whether or not to cache rules for performance.
16
+ def self.cache_rules=(val); @@cache_rules = val; end
17
+ def self.cache_rules; @@cache_rules; end
18
+ self.cache_rules = false
19
+
20
+ # The list of params that we should allow (but not require) by default. It's as if we
21
+ # said that all requests may_have these elements. By default this
22
+ # list is set to:
23
+ #
24
+ # * action
25
+ # * controller
26
+ # * commit
27
+ # * _method
28
+ #
29
+ # You can modify this list in your environment.rb if you need to. Always
30
+ # use strings, not symbols for the elements. Here's an example:
31
+ #
32
+ # AcceptParams::ParamRules.ignore_params << "orientation"
33
+ #
34
+ def self.ignore_params=(val); @@ignore_params = val; end
35
+ def self.ignore_params; @@ignore_params; end
36
+ self.ignore_params = %w( action controller commit format _method authenticity_token )
37
+
38
+ # The columns in ActiveRecord models that we should ignore by
39
+ # default when expanding an is_a directive into a series of
40
+ # must_have directives for each attribute. These are the
41
+ # attributes that are almost never present in your forms (and hence your params).
42
+ # By default this list is set to:
43
+ #
44
+ # * id
45
+ # * created_at
46
+ # * updated_at
47
+ # * created_on
48
+ # * updated_on
49
+ # * lock_version
50
+ #
51
+ # You can modify this in your environment.rb if you have common attributes
52
+ # that should always be ignored. Here's an example:
53
+ #
54
+ # AcceptParams::ParamRules.ignore_columns << "deleted_at"
55
+ #
56
+ def self.ignore_columns=(val); @@ignore_columns = val; end
57
+ def self.ignore_columns; @@ignore_columns; end
58
+ self.ignore_columns = %w( id created_at updated_at created_on updated_on lock_version )
59
+
60
+ # If unexpected params are encountered, default behavior is to raise an exception
61
+ # Setting this to true will instead just all them on through. Note this defeats
62
+ # much of the purpose of the plugin. To mitigate security issues, try setting the
63
+ # next flag to "true" if you set this to true.
64
+ def self.ignore_unexpected=(val); @@ignore_unexpected = val; end
65
+ def self.ignore_unexpected; @@ignore_unexpected; end
66
+ self.ignore_unexpected = false
67
+
68
+ # If unexpected params are encountered, remove them to prevent injection attacks.
69
+ # Note: This is only relevant if you set ignore_unexpected to true, in which case
70
+ # you can have them removed (safer) by setting this. The basic idea is that then
71
+ # an exception won't be raised, but an attacker still won't be able to inject params.
72
+ def self.remove_unexpected=(val); @@remove_unexpected = val; end
73
+ def self.remove_unexpected; @@remove_unexpected; end
74
+ self.remove_unexpected = false
75
+
76
+ # How to validate parameters, if the person doesn't specify :validate
77
+ def self.type_validations=(val); @@type_validations = val; end
78
+ def self.type_validations; @@type_validations; end
79
+ self.type_validations = {
80
+ :integer => /^-?\d+$/,
81
+ :float => /^-?(\d*\.\d+|\d+)$/,
82
+ :decimal => /^-?(\d*\.\d+|\d+)$/,
83
+ :boolean => /^(1|true|TRUE|T|Y|0|false|FALSE|F|N)$/,
84
+ :datetime => /^[-\d:T\s]+$/, # "T" is for ISO date format
85
+ }
86
+
87
+ # Global on/off for SSL
88
+ def self.ssl_enabled=(val); @@ssl_enabled = val; end
89
+ def self.ssl_enabled; @@ssl_enabled; end
90
+ self.ssl_enabled = true
91
+ end
92
+ end
93
+
94
+ require 'sinatra/accept_params/param_rules'
95
+ require 'sinatra/accept_params/helpers' # DSL for Sinatra
@@ -0,0 +1,50 @@
1
+ # See http://www.sinatrarb.com/extensions.html
2
+ module Sinatra
3
+ module AcceptParams
4
+ module Helpers
5
+ def accept_params(opts={}, &block) #:yields: param
6
+ raise NoParamsDefined, "Missing block for accept_params" unless block_given?
7
+ rules = ParamRules.new(opts)
8
+ rules.validate_request(request, session)
9
+ yield rules
10
+ rules.validate(params)
11
+ end
12
+
13
+ # Shortcut functions to tighten up security further
14
+ def accept_no_params(opts={})
15
+ accept_params(opts) {}
16
+ end
17
+
18
+ def accept_only_id(opts={})
19
+ accept_params(opts) do |p|
20
+ p.integer :id, :required => true
21
+ end
22
+ end
23
+ end
24
+
25
+ # Needed to register params handling with Sinatra
26
+ def self.registered(app)
27
+ app.helpers AcceptParams::Helpers
28
+
29
+ app.error Sinatra::AcceptParams::LoginRequired do
30
+ headers["WWW-Authenticate"] = %(Basic realm="Login required")
31
+ halt 401, "Authorization required"
32
+ end
33
+
34
+ # Have to enumerate errors, because Sinatra uses is_a? test, not inheritance
35
+ [ Sinatra::AcceptParams::ParamError,
36
+ Sinatra::AcceptParams::NoParamsDefined,
37
+ Sinatra::AcceptParams::MissingParam,
38
+ Sinatra::AcceptParams::UnexpectedParam,
39
+ Sinatra::AcceptParams::InvalidParamType,
40
+ Sinatra::AcceptParams::InvalidParamValue,
41
+ Sinatra::AcceptParams::SslRequired ].each do |cl|
42
+ app.error cl do
43
+ halt 400, request.env['sinatra.error'].message
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ register AcceptParams
50
+ end
@@ -0,0 +1,8 @@
1
+ module Sinatra
2
+ module AcceptParams
3
+ # This class holds the definition of a single param
4
+ class Definition
5
+
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,386 @@
1
+ module Sinatra
2
+ module AcceptParams
3
+ # This class is used to declare the structure of the params hash for this
4
+ # request.
5
+ class ParamRules
6
+ attr_reader :name, :parent, :children, :options, :type, :settings, :definition #:nodoc:
7
+
8
+ # TODO: Convert this to a hash of options.
9
+ def initialize(settings, type=nil, name=nil, options={}, parent=nil) # :nodoc:
10
+ if (name.nil? && !parent.nil?) || (parent.nil? && !name.nil?)
11
+ raise ArgumentError, "parent and name must both be either nil or not nil"
12
+ end
13
+ if (name.nil? && !type.nil?) || (type.nil? && !name.nil?)
14
+ raise ArgumentError, "type and name must both be either nil or not nil"
15
+ end
16
+ @type = type
17
+ @parent = parent
18
+ @children = []
19
+ @options = options
20
+
21
+ # Set default options which control behavior
22
+ @settings = {
23
+ :ignore_unexpected => AcceptParams.ignore_unexpected,
24
+ :remove_unexpected => AcceptParams.remove_unexpected,
25
+ :ignore_params => AcceptParams.ignore_params,
26
+ :ignore_columns => AcceptParams.ignore_columns,
27
+ :ssl_enabled => AcceptParams.ssl_enabled
28
+ }.merge(settings)
29
+
30
+ # This is needed for resource_definitions
31
+ @settings[:indent] ||= 0
32
+ @settings[:indent] += 2
33
+
34
+ if name.nil?
35
+ @name = nil
36
+ elsif is_model?(name)
37
+ klass = name
38
+ @name = klass.to_s.underscore
39
+ is_a klass
40
+ else
41
+ @name = name.to_s
42
+ end
43
+
44
+ # This is undocumented, and specific to SCEA
45
+ if @options.has_key? :to_id
46
+ klass = @options[:to_id]
47
+ @options[:process] = Proc.new{|v| klass.to_id(v)}
48
+ @options[:to] = "#{@name}_id"
49
+ end
50
+ end
51
+
52
+ # Validate the request object, checking the :ssl and :login flags
53
+ # This needs a big refactor, this whole class is DOG SLOW
54
+ def validate_request(request, session)
55
+ unless @settings[:ssl_enabled] == false or ENV['RACK_ENV'] == 'development'
56
+ if @settings[:ssl]
57
+ # explicitly said :ssl => true
58
+ raise SslRequired unless request.secure?
59
+ elsif @settings.has_key?(:ssl)
60
+ # explicitly said :ssl => false or :ssl => nil, so skip
61
+ else
62
+ # require SSL on anything non-GET
63
+ raise SslRequired unless request.get?
64
+ end
65
+ end
66
+
67
+ # Same thing for login_required, minus global flag
68
+ if @settings[:login]
69
+ # explicitly said :login => true
70
+ raise LoginRequired unless session[:username]
71
+ elsif @settings.has_key?(:login)
72
+ # explicitly said :login => false or :login => nil, so skip
73
+ else
74
+ # require login on anything non-GET
75
+ raise LoginRequired unless session[:username] || request.get?
76
+ end
77
+ end
78
+
79
+ # Allow nesting
80
+ def namespace(name, &block)
81
+ raise ArgumentError, "Missing block to param namespace declaration" unless block_given?
82
+ child = ParamRules.new(settings, :namespace, name, {:required => false}, self) # block not required per se
83
+ yield child
84
+ @children << child
85
+ end
86
+
87
+ # Ala pretty migrations
88
+ def string(name, options={})
89
+ param(:string, name, options)
90
+ end
91
+ def integer(name, options={})
92
+ param(:integer, name, options)
93
+ end
94
+ def float(name, options={})
95
+ param(:float, name, options)
96
+ end
97
+ def decimal(name, options={})
98
+ param(:decimal, name, options)
99
+ end
100
+ def boolean(name, options={})
101
+ param(:boolean, name, options)
102
+ end
103
+ def datetime(name, options={})
104
+ param(:datetime, name, options)
105
+ end
106
+ def text(name, options={})
107
+ param(:text, name, options)
108
+ end
109
+ def binary(name, options={})
110
+ param(:binary, name, options)
111
+ end
112
+ def array(name, options={})
113
+ param(:array, name, options)
114
+ end
115
+ def file(name, options={})
116
+ param(:file, name, options)
117
+ end
118
+
119
+ # This is a shortcut for declaring elements that represent ActiveRecord
120
+ # classes. Essentially, it creates a declaration for each
121
+ # attribute of the given model (excluding the ones in the class
122
+ # attribute ignore_columns, which is described at the top of this page).
123
+ def model(klass)
124
+ unless is_model?(klass)
125
+ raise ArgumentError, "Must supply an ActiveRecord class to the model method"
126
+ end
127
+ klass.columns.each do |c|
128
+ param(c.type, c.name, :required => !c.null, :limit => c.limit) unless ignore_column?(c)
129
+ end
130
+ end
131
+
132
+ # Is this a required params element? Implies "must_have".
133
+ def required? #:nodoc:
134
+ options[:required]
135
+ end
136
+
137
+ def namespace?
138
+ type == :namespace
139
+ end
140
+
141
+ # Returns the full name of this parameter as it would be accessed in the
142
+ # action. Example output might be "params[:person][:name]".
143
+ def canonical_name #:nodoc:
144
+ if parent.nil?
145
+ ""
146
+ elsif parent.parent.nil?
147
+ name
148
+ else
149
+ parent.canonical_name + "[#{name}]"
150
+ end
151
+ end
152
+
153
+ # Validate the given parameters against our requirements, raising
154
+ # exceptions for missing or unexpected parameters.
155
+ def validate(params) #:nodoc:
156
+ recognized_keys = validate_children(params)
157
+ unexpected_keys = params.keys - recognized_keys
158
+ if parent.nil?
159
+ # Only ignore the standard params at the top level.
160
+ unexpected_keys -= settings[:ignore_params]
161
+ end
162
+ unless unexpected_keys.empty?
163
+ # kinda hacky to get it to display correctly
164
+ unless settings[:ignore_unexpected]
165
+ basename = canonical_name
166
+ canonicals = unexpected_keys.sort.collect{|k| basename.empty? ? k : basename + "[#{k}]"}.join(', ')
167
+ s = unexpected_keys.length == 1 ? '' : 's'
168
+ raise UnexpectedParam, "Request included unexpected parameter#{s}: #{canonicals}"
169
+ end
170
+ unexpected_keys.each{|k| params.delete(k)} if settings[:remove_unexpected]
171
+ end
172
+ end
173
+
174
+ # Create a new param
175
+ def param(type, name, options)
176
+ @children << ParamRules.new(settings, type.to_sym, name, options, self)
177
+ end
178
+
179
+ private
180
+
181
+ # Should we ignore this ActiveRecord column?
182
+ def ignore_column?(column)
183
+ settings[:ignore_columns].detect { |name| name.to_s == column.name }
184
+ end
185
+
186
+ # Determine if the given class is an ActiveRecord model.
187
+ def is_model?(klass)
188
+ klass.respond_to?(:ancestors) &&
189
+ klass.ancestors.detect {|a| a == ActiveRecord::Base}
190
+ end
191
+
192
+ # Remove the given children.
193
+ def remove_child(*names)
194
+ names.each do |name|
195
+ children.delete_if { |child| child.name == name.to_s }
196
+ end
197
+ end
198
+
199
+ # Validate our children against the given params, looking for missing
200
+ # required elements. Returns a list of the keys that we were able to
201
+ # recognize.
202
+ def validate_children(params)
203
+ recognized_keys = []
204
+ children.each do |child|
205
+ #puts ">>>>>>>>>> child.name = #{child.canonical_name}"
206
+ if child.namespace?
207
+ recognized_keys << child.name
208
+ # NOTE: Can't get fancy and do this ||= w/i the below func call, due to
209
+ # an apparent oddity of Ruby's scoping for method args
210
+ params[child.name] ||= HashWithIndifferentAccess.new # create holder for subelements if missing
211
+ validate_child(child, params[child.name])
212
+ elsif params.has_key?(child.name)
213
+ recognized_keys << child.name
214
+ validate_child(child, params[child.name])
215
+ validate_value_and_type_cast!(child, params)
216
+ elsif child.required?
217
+ raise MissingParam, "Request params missing required parameter '#{child.canonical_name}'"
218
+ else
219
+ # For setting defaults on missing parameters
220
+ recognized_keys << child.name
221
+ validate_value_and_type_cast!(child, params)
222
+ end
223
+
224
+ # Finally, handle key renaming
225
+ if new_name = child.options[:to]
226
+ # Removed this because it causes havok with :to_id and will_paginate.
227
+ # Not needed anyways, since we just overwrite it right afterwards.
228
+ # if params.has_key? new_name
229
+ # raise UnexpectedParam, "Request included destination parameter '#{new_name}'"
230
+ # end
231
+ params[new_name] = params.delete(child.name)
232
+ recognized_keys << new_name.to_s
233
+ end
234
+ end
235
+ #puts "!!!!!!!!! DONE: params[:filters] = #{params[:filters].inspect}; #{params[:filters].object_id}"
236
+ recognized_keys
237
+ end
238
+
239
+ # Validate this child against its matching value. In addition, manipulate the params
240
+ # hash as-needed to set any applicable default values.
241
+ def validate_child(child, value)
242
+ if child.children.empty?
243
+ if value.is_a?(Hash)
244
+ raise UnexpectedParam, "Request parameter '#{child.canonical_name}' is a hash, but wasn't expecting it"
245
+ end
246
+ else
247
+ if value.is_a?(Hash)
248
+ #puts "????????? NEST: #{value.inspect} (#{value.object_id})"
249
+ child.validate(value) # recurse
250
+ else
251
+ raise InvalidParamValue, "Expected parameter '#{child.canonical_name}' to be a nested hash"
252
+ end
253
+ end
254
+ end
255
+
256
+ def validate_value_and_type_cast!(child, params)
257
+ return true if child.namespace?
258
+ value = params[child.name] # we may be recursive, eg, params[:filters][:player_creation_type]
259
+ #puts "@@@@@@@@@@@@ VALUE(#{child.canonical_name}) = #{value.inspect}"
260
+
261
+ # XXX Special catch for pagination with :to_id fields, since "player_creation_type"
262
+ # becomes player_creation_type_id (with the correct value) on subsequent pages
263
+ #puts "@@@ #{child.canonical_name}: if #{value.nil?} and #{options[:to]} and #{params[options[:to]]} (#{options.inspect})"
264
+ if value.nil? and to = child.options[:to] and params[to]
265
+ value = params[to]
266
+ elsif value.nil?
267
+ if child.options.has_key?(:default)
268
+ if child.options[:default].is_a? Proc
269
+ begin
270
+ value = child.options[:default].call
271
+ rescue Exception => e
272
+ # Rebrand exceptions so top-level can catch
273
+ raise InvalidParamValue, e.to_s
274
+ end
275
+ else
276
+ value = child.options[:default]
277
+ end
278
+ elsif child.required?
279
+ raise InvalidParamValue, "Value for parameter '#{child.canonical_name}' is null or missing"
280
+ else
281
+ # If no default, that means it's *really* optional
282
+ return true
283
+ end
284
+ elsif child.options.has_key?(:process)
285
+ # Only call the process method if we're *not* using a default value
286
+ # Must *NOT* type cast this value, or else it will be cast back to the
287
+ # input value type (eg, string), rather than the :to_id type (integer)
288
+ begin
289
+ #puts ">>>>>>> #{value.inspect}, #{params.inspect}"
290
+ value = child.options[:process].call(value)
291
+ #puts ">>>>>>> #{value.inspect}, #{params.inspect}"
292
+ rescue Exception => e
293
+ # Rebrand exceptions so top-level can catch
294
+ raise InvalidParamValue, e.to_s
295
+ end
296
+ elsif child.type == :array
297
+ value = value.split(',') if value.is_a? String # accept comma,delimited,string also
298
+ unless value.is_a? Array
299
+ raise InvalidParamType, "Value for parameter '#{child.canonical_name}' (#{value}) is of the wrong type (expected #{child.type})"
300
+ end
301
+ else
302
+ # Should this be at a higher level?
303
+ if child.options[:validate] && value.to_s !~ child.options[:validate]
304
+ format_info = child.options[:format] and format_info = " (format: #{format_info})"
305
+ raise InvalidParamValue, "Invalid value for parameter '#{name}'#{format_info}"
306
+ elsif validation = AcceptParams.type_validations[child.type]
307
+ # Use built-in sanity check if we have it
308
+ unless value.to_s =~ validation
309
+ raise InvalidParamType, "Value for parameter '#{child.canonical_name}' (#{value}) is of the wrong type (expected #{child.type})"
310
+ end
311
+ end
312
+
313
+ # Typecast only NON-defaults; assume the programmer was smart enough
314
+ # to say :default => 4 rather than :default => "4" if using defaults
315
+ value = type_cast_value(child.type, value)
316
+ optional_extended_validations(child.canonical_name, value, child.options)
317
+ end
318
+
319
+ # Overwrite our original value, to make params safe
320
+ params[child.name] = value
321
+ #puts "+++++++++ #{child.canonical_name}: params[#{child.name}] = #{value.inspect} (#{params.object_id})"
322
+ end
323
+
324
+ def type_cast_value(type, value)
325
+ case type
326
+ when :integer
327
+ value.to_i
328
+ when :float, :decimal
329
+ value.to_f
330
+ when :string
331
+ value.to_s
332
+ when :boolean
333
+ if value.is_a? TrueClass
334
+ true
335
+ elsif value.is_a? FalseClass
336
+ false
337
+ else
338
+ case value.to_s
339
+ when /^(1|true|TRUE|T|Y)$/
340
+ true
341
+ when /^(0|false|FALSE|F|N)$/
342
+ false
343
+ else
344
+ raise InvalidParamValue, "Could not typecast boolean value '#{value.to_s}' to true or false"
345
+ end
346
+ end
347
+ when :binary, :array, :file
348
+ value
349
+ else
350
+ value.to_s
351
+ end
352
+ end
353
+
354
+ def optional_extended_validations(name, value, options)
355
+ # XXX This probably needs to go into integer/float-specific code somewhere
356
+ if options[:minvalue] && value.to_i < options[:minvalue]
357
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is less than minimum value (#{options[:minvalue]})"
358
+ end
359
+ if options[:maxvalue] && value.to_i > options[:maxvalue]
360
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is more than maximum value (#{options[:maxvalue]})"
361
+ end
362
+
363
+ # This is general-purpose, but still feels like it should be in a separate method
364
+ if options[:in] && !options[:in].include?(value)
365
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is not in the allowed set of values"
366
+ end
367
+
368
+ # XXX This probably needs to go into string-specific code somewhere
369
+ if options[:maxlength] && value.length > options[:maxlength]
370
+ raise InvalidParamValue, "Length of parameter '#{name}' (#{value.length}) is longer than maximum length (#{options[:maxlength]})"
371
+ end
372
+
373
+ # XXX This probably needs to go into string-specific code somewhere
374
+ if options[:minlength] && value.length < options[:minlength]
375
+ raise InvalidParamValue, "Length of parameter '#{name}' (#{value.length}) is smaller than minimum length (#{options[:minlength]})"
376
+ end
377
+
378
+ # Finally, if :null => false, this is a special sanity check that it can't be empty
379
+ # This is designed to catch cases where the default/etc are null; it's a double-condom for programmers
380
+ if (value.nil? || value == "") && (options.has_key?(:null) && options[:null] == false)
381
+ raise InvalidParamValue, "Value for parameter '#{name}' is null or missing"
382
+ end
383
+ end
384
+ end
385
+ end
386
+ end
@@ -0,0 +1,130 @@
1
+ require File.expand_path 'spec_helper', File.dirname(__FILE__)
2
+
3
+ class Application < Sinatra::Base
4
+ register Sinatra::AcceptParams
5
+
6
+ set :raise_errors, false
7
+ set :show_exceptions, false
8
+
9
+ get '/search' do
10
+ accept_params do |p|
11
+ p.integer :page, :default => 1, :minvalue => 1
12
+ p.integer :limit, :default => 20, :maxvalue => 100
13
+ p.boolean :wildcard, :default => false
14
+ p.string :search, :required => true
15
+ p.float :timeout, :default => 3.5
16
+ end
17
+ params_dump
18
+ end
19
+
20
+ get '/users' do
21
+ accept_no_params
22
+ end
23
+
24
+ get '/posts/:id' do
25
+ accept_only_id
26
+ end
27
+ end
28
+
29
+ class Bacon::Context
30
+ include Rack::Test::Methods
31
+ def app
32
+ Application # our application
33
+ end
34
+ end
35
+
36
+ describe "Sinatra::AcceptParams" do
37
+ it "should provide settings to control the lib" do
38
+ Sinatra::AcceptParams.cache_rules.should == false
39
+ Sinatra::AcceptParams.cache_rules = true
40
+ Sinatra::AcceptParams.cache_rules.should == true
41
+ Sinatra::AcceptParams.cache_rules = false
42
+ Sinatra::AcceptParams.cache_rules.should == false
43
+
44
+ Sinatra::AcceptParams.ignore_params.should == %w( action controller commit format _method authenticity_token )
45
+ Sinatra::AcceptParams.ignore_params << 'ricky_bobby'
46
+ Sinatra::AcceptParams.ignore_params.should == %w( action controller commit format _method authenticity_token ricky_bobby )
47
+
48
+ Sinatra::AcceptParams.ignore_columns.should == %w( id created_at updated_at created_on updated_on lock_version )
49
+ Sinatra::AcceptParams.ignore_columns << 'shake_and_bake'
50
+ Sinatra::AcceptParams.ignore_columns.should == %w( id created_at updated_at created_on updated_on lock_version shake_and_bake )
51
+
52
+ Sinatra::AcceptParams.ignore_unexpected.should == false
53
+ Sinatra::AcceptParams.ignore_unexpected = true
54
+ Sinatra::AcceptParams.ignore_unexpected.should == true
55
+ Sinatra::AcceptParams.ignore_unexpected = false
56
+ Sinatra::AcceptParams.ignore_unexpected.should == false
57
+
58
+ Sinatra::AcceptParams.remove_unexpected.should == false
59
+ Sinatra::AcceptParams.remove_unexpected = true
60
+ Sinatra::AcceptParams.remove_unexpected.should == true
61
+ Sinatra::AcceptParams.remove_unexpected = false
62
+ Sinatra::AcceptParams.remove_unexpected.should == false
63
+
64
+ Sinatra::AcceptParams.type_validations[:cal_jr] = /ricky_bobby/
65
+ Sinatra::AcceptParams.type_validations[:cal_jr].should == /ricky_bobby/
66
+
67
+ Sinatra::AcceptParams.ssl_enabled.should == true
68
+ Sinatra::AcceptParams.ssl_enabled = false
69
+ Sinatra::AcceptParams.ssl_enabled.should == false
70
+ Sinatra::AcceptParams.ssl_enabled = true
71
+ Sinatra::AcceptParams.ssl_enabled.should == true
72
+ end
73
+
74
+ it "should handle accept_params blocks" do
75
+ get '/search'
76
+ last_response.status.should == 400
77
+ last_response.body.should == %q(Request params missing required parameter 'search')
78
+
79
+ get '/search', :page => 'Yes'
80
+ last_response.status.should == 400
81
+ last_response.body.should == %q(Value for parameter 'page' (Yes) is of the wrong type (expected integer))
82
+
83
+ get '/search', :wildcard => 15
84
+ last_response.status.should == 400
85
+ last_response.body.should == %q(Value for parameter 'wildcard' (15) is of the wrong type (expected boolean))
86
+
87
+ get '/search', :page => 0
88
+ last_response.status.should == 400
89
+ last_response.body.should == %q(Value for parameter 'page' (0) is less than minimum value (1))
90
+
91
+ get '/search', :limit => 900000
92
+ last_response.status.should == 400
93
+ last_response.body.should == %q(Value for parameter 'limit' (900000) is more than maximum value (100))
94
+
95
+ get '/search', :search => 'foot'
96
+ last_response.status.should == 200
97
+ last_response.body.should == "limit=20; page=1; search=foot; timeout=3.5; wildcard=false"
98
+
99
+ get '/search', :search => 'taco grande', :wildcard => 'true'
100
+ last_response.status.should == 200
101
+ last_response.body.should == "limit=20; page=1; search=taco grande; timeout=3.5; wildcard=true"
102
+
103
+ get '/search', :limit => 100, :wildcard => 0, :search => 'string', :timeout => '19.2433'
104
+ last_response.status.should == 200
105
+ last_response.body.should == "limit=100; page=1; search=string; timeout=19.2433; wildcard=false"
106
+
107
+ get '/search', :a => 3, :b => 4, :search => 'bar'
108
+ last_response.status.should == 400
109
+ last_response.body.should == %q(Request included unexpected parameters: a, b)
110
+ end
111
+
112
+ it "should handle accept_no_params call" do
113
+ get '/users', :limit => 1
114
+ last_response.status.should == 400
115
+ last_response.body.should == %q(Request included unexpected parameter: limit)
116
+
117
+ get '/users'
118
+ last_response.status.should == 200
119
+ end
120
+
121
+
122
+ it "should handle accept_only_id call" do
123
+ get '/posts/blarp'
124
+ last_response.status.should == 400
125
+ last_response.body.should == %q(Value for parameter 'id' (blarp) is of the wrong type (expected integer))
126
+
127
+ get '/posts/1'
128
+ last_response.status.should == 200
129
+ end
130
+ end
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'bacon'
3
+ require 'rack/test'
4
+
5
+ $LOAD_PATH.unshift(File.expand_path File.dirname(__FILE__))
6
+ $LOAD_PATH.unshift(File.expand_path File.join(File.dirname(__FILE__), '..', 'lib'))
7
+ require 'sinatra'
8
+ require 'sinatra/accept_params'
9
+
10
+ Bacon.summary_on_exit
11
+
12
+ def params_dump
13
+ params.keys.sort.collect{|k| "#{k}=#{params[k]}"} * '; '
14
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-accept-params
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Nate Wiger
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-12 00:00:00 -07:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
21
+ name: bacon
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ segments:
28
+ - 0
29
+ version: "0"
30
+ type: :development
31
+ version_requirements: *id001
32
+ description: Parameter whitelisting for Sinatra. Provides validation, defaults, and post-processing.
33
+ email: nate@wiger.org
34
+ executables: []
35
+
36
+ extensions: []
37
+
38
+ extra_rdoc_files:
39
+ - LICENSE
40
+ - README.md
41
+ files:
42
+ - .document
43
+ - .gitignore
44
+ - LICENSE
45
+ - README.md
46
+ - Rakefile
47
+ - VERSION
48
+ - lib/sinatra/accept_params.rb
49
+ - lib/sinatra/accept_params/helpers.rb
50
+ - lib/sinatra/accept_params/param.rb
51
+ - lib/sinatra/accept_params/param_rules.rb
52
+ - spec/accept_params_spec.rb
53
+ - spec/spec_helper.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/nateware/sinatra-accept-params
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options:
60
+ - --charset=UTF-8
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ segments:
75
+ - 0
76
+ version: "0"
77
+ requirements: []
78
+
79
+ rubyforge_project:
80
+ rubygems_version: 1.3.6
81
+ signing_key:
82
+ specification_version: 3
83
+ summary: Parameter whitelisting for Sinatra
84
+ test_files:
85
+ - spec/accept_params_spec.rb
86
+ - spec/spec_helper.rb