sinatra-accept-params-d1plo1d 0.1.1

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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
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.
data/README.md ADDED
@@ -0,0 +1,68 @@
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 of 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
+ Copyright (c) 2008-2010 [Nate Wiger](http://nateware.com). All Rights Reserved.
67
+ This code is released under the Artistic License.
68
+
data/Rakefile ADDED
@@ -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-d1plo1d"
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.1
@@ -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,415 @@
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, options={}, &block)
81
+ raise ArgumentError, "Missing block to param namespace declaration" unless block_given?
82
+ child = ParamRules.new(settings, :namespace, name, options.merge(: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
+
200
+ def validate_child_name(child, name, parent_params)
201
+ # validate child name (if :process_name is declared)
202
+ if child.options.has_key?(:process_name) == true then
203
+ value = parent_params.delete(name);
204
+ name = child.options[:process_name].call(name)
205
+ parent_params[name] = value;
206
+ end
207
+
208
+ return name.to_s
209
+ end
210
+
211
+
212
+ # Validate our children against the given params, looking for missing
213
+ # required elements. Returns a list of the keys that we were able to
214
+ # recognize.
215
+ def validate_children(params)
216
+ recognized_keys = []
217
+ children.each do |child|
218
+ #puts ">>>>>>>>>> child.name = #{child.canonical_name}"
219
+ if child.namespace?
220
+ valid_namespace_names(child.name, params).each do |name|
221
+ name = validate_child_name(child, name, params)
222
+ recognized_keys << name
223
+ #puts "\n\nnNANAAAAMMMEEE: " +name.to_s
224
+ # NOTE: Can't get fancy and do this ||= w/i the below func call, due to
225
+ # an apparent oddity of Ruby's scoping for method args
226
+ params[name] ||= HashWithIndifferentAccess.new # create holder for subelements if missing
227
+ validate_child(child, params[name])
228
+ end
229
+ elsif params.has_key?(child.name)
230
+ name = validate_child_name(child, child.name, params)
231
+ recognized_keys << name
232
+ validate_child(child, params[name])
233
+ validate_value_and_type_cast!(child, name, params)
234
+ elsif child.required?
235
+ raise MissingParam, "Request params missing required parameter '#{child.canonical_name}'"
236
+ else
237
+ # For setting defaults on missing parameters
238
+ recognized_keys << child.name
239
+ validate_value_and_type_cast!(child, child.name, params)
240
+ end
241
+
242
+ # Finally, handle key renaming
243
+ if new_name = child.options[:to]
244
+ # Removed this because it causes havok with :to_id and will_paginate.
245
+ # Not needed anyways, since we just overwrite it right afterwards.
246
+ # if params.has_key? new_name
247
+ # raise UnexpectedParam, "Request included destination parameter '#{new_name}'"
248
+ # end
249
+ params[new_name] = params.delete(child.name)
250
+ recognized_keys << new_name.to_s
251
+ end
252
+ end
253
+ #puts "!!!!!!!!! DONE: params[:filters] = #{params[:filters].inspect}; #{params[:filters].object_id}"
254
+ recognized_keys
255
+ end
256
+
257
+ # Validate this child against its matching value. In addition, manipulate the params
258
+ # hash as-needed to set any applicable default values.
259
+ def validate_child(child, value)
260
+ if child.children.empty?
261
+ if value.is_a?(Hash)
262
+ raise UnexpectedParam, "Request parameter '#{child.canonical_name}' is a hash, but wasn't expecting it"
263
+ end
264
+ else
265
+ if value.is_a?(Hash)
266
+ #puts "????????? NEST: #{value.inspect} (#{value.object_id})"
267
+ child.validate(value) # recurse
268
+ else
269
+ raise InvalidParamValue, "Expected parameter '#{child.canonical_name}' to be a nested hash"
270
+ end
271
+ end
272
+ end
273
+
274
+ # validates a param's hash identifier
275
+ def valid_namespace_names(name_regex, params)
276
+ valid_names = []
277
+ params.each_key do |param_name|
278
+ match = param_name.match(name_regex)
279
+ #puts param_name + " match: " + (param_name.match(name_regex)||[""])[0] + " regex: " + name_regex
280
+ valid_names.push param_name if match != nil && match[0] == param_name
281
+ end
282
+ return valid_names
283
+ end
284
+
285
+ def validate_value_and_type_cast!(child, name, params)
286
+ return true if child.namespace?
287
+ value = params[name] # we may be recursive, eg, params[:filters][:player_creation_type]
288
+ #puts "@@@@@@@@@@@@ VALUE(#{child.canonical_name}) = #{value.inspect}"
289
+
290
+ # XXX Special catch for pagination with :to_id fields, since "player_creation_type"
291
+ # becomes player_creation_type_id (with the correct value) on subsequent pages
292
+ #puts "@@@ #{child.canonical_name}: if #{value.nil?} and #{options[:to]} and #{params[options[:to]]} (#{options.inspect})"
293
+ if value.nil? and to = child.options[:to] and params[to]
294
+ value = params[to]
295
+ elsif value.nil?
296
+ if child.options.has_key?(:default)
297
+ if child.options[:default].is_a? Proc
298
+ begin
299
+ value = child.options[:default].call
300
+ rescue Exception => e
301
+ # Rebrand exceptions so top-level can catch
302
+ raise InvalidParamValue, e.to_s
303
+ end
304
+ else
305
+ value = child.options[:default]
306
+ end
307
+ elsif child.required?
308
+ raise InvalidParamValue, "Value for parameter '#{child.canonical_name}' is null or missing"
309
+ else
310
+ # If no default, that means it's *really* optional
311
+ return true
312
+ end
313
+ elsif child.options.has_key?(:process)
314
+ # Only call the process method if we're *not* using a default value
315
+ # Must *NOT* type cast this value, or else it will be cast back to the
316
+ # input value type (eg, string), rather than the :to_id type (integer)
317
+ begin
318
+ #puts ">>>>>>> #{value.inspect}, #{params.inspect}"
319
+ value = child.options[:process].call(value)
320
+ #puts ">>>>>>> #{value.inspect}, #{params.inspect}"
321
+ rescue Exception => e
322
+ # Rebrand exceptions so top-level can catch
323
+ raise InvalidParamValue, e.to_s
324
+ end
325
+ elsif child.type == :array
326
+ value = value.split(',') if value.is_a? String # accept comma,delimited,string also
327
+ unless value.is_a? Array
328
+ raise InvalidParamType, "Value for parameter '#{child.canonical_name}' (#{value}) is of the wrong type (expected #{child.type})"
329
+ end
330
+ else
331
+ # Should this be at a higher level?
332
+ if child.options[:validate] && value.to_s !~ child.options[:validate]
333
+ format_info = child.options[:format] and format_info = " (format: #{format_info})"
334
+ raise InvalidParamValue, "Invalid value for parameter '#{name}'#{format_info}"
335
+ elsif validation = AcceptParams.type_validations[child.type]
336
+ # Use built-in sanity check if we have it
337
+ unless value.to_s =~ validation
338
+ raise InvalidParamType, "Value for parameter '#{child.canonical_name}' (#{value}) is of the wrong type (expected #{child.type})"
339
+ end
340
+ end
341
+
342
+ # Typecast only NON-defaults; assume the programmer was smart enough
343
+ # to say :default => 4 rather than :default => "4" if using defaults
344
+ value = type_cast_value(child.type, value)
345
+ optional_extended_validations(child.canonical_name, value, child.options)
346
+ end
347
+
348
+ # Overwrite our original value, to make params safe
349
+ params[name] = value
350
+ #puts "+++++++++ #{child.canonical_name}: params[#{child.name}] = #{value.inspect} (#{params.object_id})"
351
+ end
352
+
353
+ def type_cast_value(type, value)
354
+ case type
355
+ when :integer
356
+ value.to_i
357
+ when :float, :decimal
358
+ value.to_f
359
+ when :string
360
+ value.to_s
361
+ when :boolean
362
+ if value.is_a? TrueClass
363
+ true
364
+ elsif value.is_a? FalseClass
365
+ false
366
+ else
367
+ case value.to_s
368
+ when /^(1|true|TRUE|T|Y)$/
369
+ true
370
+ when /^(0|false|FALSE|F|N)$/
371
+ false
372
+ else
373
+ raise InvalidParamValue, "Could not typecast boolean value '#{value.to_s}' to true or false"
374
+ end
375
+ end
376
+ when :binary, :array, :file
377
+ value
378
+ else
379
+ value.to_s
380
+ end
381
+ end
382
+
383
+ def optional_extended_validations(name, value, options)
384
+ # XXX This probably needs to go into integer/float-specific code somewhere
385
+ if options[:minvalue] && value.to_i < options[:minvalue]
386
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is less than minimum value (#{options[:minvalue]})"
387
+ end
388
+ if options[:maxvalue] && value.to_i > options[:maxvalue]
389
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is more than maximum value (#{options[:maxvalue]})"
390
+ end
391
+
392
+ # This is general-purpose, but still feels like it should be in a separate method
393
+ if options[:in] && !options[:in].include?(value)
394
+ raise InvalidParamValue, "Value for parameter '#{name}' (#{value}) is not in the allowed set of values"
395
+ end
396
+
397
+ # XXX This probably needs to go into string-specific code somewhere
398
+ if options[:maxlength] && value.length > options[:maxlength]
399
+ raise InvalidParamValue, "Length of parameter '#{name}' (#{value.length}) is longer than maximum length (#{options[:maxlength]})"
400
+ end
401
+
402
+ # XXX This probably needs to go into string-specific code somewhere
403
+ if options[:minlength] && value.length < options[:minlength]
404
+ raise InvalidParamValue, "Length of parameter '#{name}' (#{value.length}) is smaller than minimum length (#{options[:minlength]})"
405
+ end
406
+
407
+ # Finally, if :null => false, this is a special sanity check that it can't be empty
408
+ # This is designed to catch cases where the default/etc are null; it's a double-condom for programmers
409
+ if (value.nil? || value == "") && (options.has_key?(:null) && options[:null] == false)
410
+ raise InvalidParamValue, "Value for parameter '#{name}' is null or missing"
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,137 @@
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
+ begin
11
+ accept_params do |p|
12
+ p.integer :page, :default => 1, :minvalue => 1
13
+ p.integer :limit, :default => 20, :maxvalue => 100
14
+ p.boolean :wildcard, :default => false
15
+ p.string :search, :required => true
16
+ p.float :timeout, :default => 3.5
17
+ end
18
+ rescue Exception => e
19
+ puts "\n<error>"
20
+ puts "\n\n\n"+e.to_s
21
+ puts "\n</error>"
22
+ raise e
23
+ end
24
+ params_dump
25
+ end
26
+
27
+ get '/users' do
28
+ accept_no_params
29
+ end
30
+
31
+ get '/posts/:id' do
32
+ accept_only_id
33
+ end
34
+ end
35
+
36
+ class Bacon::Context
37
+ include Rack::Test::Methods
38
+ def app
39
+ Application # our application
40
+ end
41
+ end
42
+
43
+ describe "Sinatra::AcceptParams" do
44
+ it "should provide settings to control the lib" do
45
+ Sinatra::AcceptParams.cache_rules.should == false
46
+ Sinatra::AcceptParams.cache_rules = true
47
+ Sinatra::AcceptParams.cache_rules.should == true
48
+ Sinatra::AcceptParams.cache_rules = false
49
+ Sinatra::AcceptParams.cache_rules.should == false
50
+
51
+ Sinatra::AcceptParams.ignore_params.should == %w( action controller commit format _method authenticity_token )
52
+ Sinatra::AcceptParams.ignore_params << 'ricky_bobby'
53
+ Sinatra::AcceptParams.ignore_params.should == %w( action controller commit format _method authenticity_token ricky_bobby )
54
+
55
+ Sinatra::AcceptParams.ignore_columns.should == %w( id created_at updated_at created_on updated_on lock_version )
56
+ Sinatra::AcceptParams.ignore_columns << 'shake_and_bake'
57
+ Sinatra::AcceptParams.ignore_columns.should == %w( id created_at updated_at created_on updated_on lock_version shake_and_bake )
58
+
59
+ Sinatra::AcceptParams.ignore_unexpected.should == false
60
+ Sinatra::AcceptParams.ignore_unexpected = true
61
+ Sinatra::AcceptParams.ignore_unexpected.should == true
62
+ Sinatra::AcceptParams.ignore_unexpected = false
63
+ Sinatra::AcceptParams.ignore_unexpected.should == false
64
+
65
+ Sinatra::AcceptParams.remove_unexpected.should == false
66
+ Sinatra::AcceptParams.remove_unexpected = true
67
+ Sinatra::AcceptParams.remove_unexpected.should == true
68
+ Sinatra::AcceptParams.remove_unexpected = false
69
+ Sinatra::AcceptParams.remove_unexpected.should == false
70
+
71
+ Sinatra::AcceptParams.type_validations[:cal_jr] = /ricky_bobby/
72
+ Sinatra::AcceptParams.type_validations[:cal_jr].should == /ricky_bobby/
73
+
74
+ Sinatra::AcceptParams.ssl_enabled.should == true
75
+ Sinatra::AcceptParams.ssl_enabled = false
76
+ Sinatra::AcceptParams.ssl_enabled.should == false
77
+ Sinatra::AcceptParams.ssl_enabled = true
78
+ Sinatra::AcceptParams.ssl_enabled.should == true
79
+ end
80
+
81
+ it "should handle accept_params blocks" do
82
+ get '/search'
83
+ last_response.status.should == 400
84
+ last_response.body.should == %q(Request params missing required parameter 'search')
85
+
86
+ get '/search', :page => 'Yes'
87
+ last_response.status.should == 400
88
+ last_response.body.should == %q(Value for parameter 'page' (Yes) is of the wrong type (expected integer))
89
+
90
+ get '/search', :wildcard => 15
91
+ last_response.status.should == 400
92
+ last_response.body.should == %q(Value for parameter 'wildcard' (15) is of the wrong type (expected boolean))
93
+
94
+ get '/search', :page => 0
95
+ last_response.status.should == 400
96
+ last_response.body.should == %q(Value for parameter 'page' (0) is less than minimum value (1))
97
+
98
+ get '/search', :limit => 900000
99
+ last_response.status.should == 400
100
+ last_response.body.should == %q(Value for parameter 'limit' (900000) is more than maximum value (100))
101
+
102
+ get '/search', :search => 'foot'
103
+ last_response.status.should == 200
104
+ last_response.body.should == "limit=20; page=1; search=foot; timeout=3.5; wildcard=false"
105
+
106
+ get '/search', :search => 'taco grande', :wildcard => 'true'
107
+ last_response.status.should == 200
108
+ last_response.body.should == "limit=20; page=1; search=taco grande; timeout=3.5; wildcard=true"
109
+
110
+ get '/search', :limit => 100, :wildcard => 0, :search => 'string', :timeout => '19.2433'
111
+ last_response.status.should == 200
112
+ last_response.body.should == "limit=100; page=1; search=string; timeout=19.2433; wildcard=false"
113
+
114
+ get '/search', :a => 3, :b => 4, :search => 'bar'
115
+ last_response.status.should == 400
116
+ last_response.body.should == %q(Request included unexpected parameters: a, b)
117
+ end
118
+
119
+ it "should handle accept_no_params call" do
120
+ get '/users', :limit => 1
121
+ last_response.status.should == 400
122
+ last_response.body.should == %q(Request included unexpected parameter: limit)
123
+
124
+ get '/users'
125
+ last_response.status.should == 200
126
+ end
127
+
128
+
129
+ it "should handle accept_only_id call" do
130
+ get '/posts/blarp'
131
+ last_response.status.should == 400
132
+ last_response.body.should == %q(Value for parameter 'id' (blarp) is of the wrong type (expected integer))
133
+
134
+ get '/posts/1'
135
+ last_response.status.should == 200
136
+ end
137
+ 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,87 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-accept-params-d1plo1d
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 1
9
+ version: 0.1.1
10
+ platform: ruby
11
+ authors:
12
+ - Nate Wiger
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-02-23 00:00:00 -05: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
+ none: false
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ version: "0"
31
+ type: :development
32
+ version_requirements: *id001
33
+ description: Parameter whitelisting for Sinatra. Provides validation, defaults, and post-processing.
34
+ email: nate@wiger.org
35
+ executables: []
36
+
37
+ extensions: []
38
+
39
+ extra_rdoc_files:
40
+ - LICENSE
41
+ - README.md
42
+ files:
43
+ - .document
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_rules.rb
51
+ - spec/accept_params_spec.rb
52
+ - spec/spec_helper.rb
53
+ has_rdoc: true
54
+ homepage: http://github.com/nateware/sinatra-accept-params
55
+ licenses: []
56
+
57
+ post_install_message:
58
+ rdoc_options: []
59
+
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ none: false
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ segments:
68
+ - 0
69
+ version: "0"
70
+ required_rubygems_version: !ruby/object:Gem::Requirement
71
+ none: false
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ segments:
76
+ - 0
77
+ version: "0"
78
+ requirements: []
79
+
80
+ rubyforge_project:
81
+ rubygems_version: 1.3.7
82
+ signing_key:
83
+ specification_version: 3
84
+ summary: Parameter whitelisting for Sinatra
85
+ test_files:
86
+ - spec/accept_params_spec.rb
87
+ - spec/spec_helper.rb