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.
- data/.document +5 -0
- data/.gitignore +21 -0
- data/LICENSE +20 -0
- data/README.md +69 -0
- data/Rakefile +53 -0
- data/VERSION +1 -0
- data/lib/sinatra/accept_params.rb +95 -0
- data/lib/sinatra/accept_params/helpers.rb +50 -0
- data/lib/sinatra/accept_params/param.rb +8 -0
- data/lib/sinatra/accept_params/param_rules.rb +386 -0
- data/spec/accept_params_spec.rb +130 -0
- data/spec/spec_helper.rb +14 -0
- metadata +86 -0
data/.document
ADDED
data/.gitignore
ADDED
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,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
|
+
|
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"
|
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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|