sinatra-accept-params 0.1.0

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