rack-contrib 0.9.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.
Potentially problematic release.
This version of rack-contrib might be problematic. Click here for more details.
- data/COPYING +18 -0
- data/README.rdoc +60 -0
- data/Rakefile +92 -0
- data/lib/rack/contrib/bounce_favicon.rb +16 -0
- data/lib/rack/contrib/callbacks.rb +37 -0
- data/lib/rack/contrib/etag.rb +20 -0
- data/lib/rack/contrib/evil.rb +12 -0
- data/lib/rack/contrib/garbagecollector.rb +14 -0
- data/lib/rack/contrib/jsonp.rb +38 -0
- data/lib/rack/contrib/lighttpd_script_name_fix.rb +16 -0
- data/lib/rack/contrib/locale.rb +31 -0
- data/lib/rack/contrib/mailexceptions.rb +120 -0
- data/lib/rack/contrib/nested_params.rb +143 -0
- data/lib/rack/contrib/post_body_content_type_parser.rb +40 -0
- data/lib/rack/contrib/proctitle.rb +30 -0
- data/lib/rack/contrib/profiler.rb +106 -0
- data/lib/rack/contrib/route_exceptions.rb +48 -0
- data/lib/rack/contrib/sendfile.rb +142 -0
- data/lib/rack/contrib/time_zone.rb +25 -0
- data/lib/rack/contrib.rb +25 -0
- data/rack-contrib.gemspec +68 -0
- data/test/mail_settings.rb +12 -0
- data/test/spec_rack_callbacks.rb +65 -0
- data/test/spec_rack_contrib.rb +8 -0
- data/test/spec_rack_etag.rb +23 -0
- data/test/spec_rack_evil.rb +19 -0
- data/test/spec_rack_garbagecollector.rb +13 -0
- data/test/spec_rack_jsonp.rb +21 -0
- data/test/spec_rack_lighttpd_script_name_fix.rb +16 -0
- data/test/spec_rack_mailexceptions.rb +97 -0
- data/test/spec_rack_nested_params.rb +46 -0
- data/test/spec_rack_post_body_content_type_parser.rb +32 -0
- data/test/spec_rack_proctitle.rb +26 -0
- data/test/spec_rack_profiler.rb +32 -0
- data/test/spec_rack_sendfile.rb +86 -0
- metadata +144 -0
data/COPYING
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2008 The Committers
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
= Contributed Rack Middleware and Utilities
|
2
|
+
|
3
|
+
This package includes a variety of add-on components for Rack, a Ruby web server
|
4
|
+
interface:
|
5
|
+
|
6
|
+
* Rack::ETag - Automatically sets the ETag header on all String bodies.
|
7
|
+
* Rack::JSONP - Adds JSON-P support by stripping out the callback param
|
8
|
+
and padding the response with the appropriate callback format.
|
9
|
+
* Rack::LighttpdScriptNameFix - Fixes how lighttpd sets the SCRIPT_NAME
|
10
|
+
and PATH_INFO variables in certain configurations.
|
11
|
+
* Rack::Locale - Detects the client locale using the Accept-Language request
|
12
|
+
header and sets a rack.locale variable in the environment.
|
13
|
+
* Rack::MailExceptions - Rescues exceptions raised from the app and
|
14
|
+
sends a useful email with the exception, stacktrace, and contents of the
|
15
|
+
environment.
|
16
|
+
* Rack::NestedParams - parses form params with subscripts (e.g., * "post[title]=Hello")
|
17
|
+
into a nested/recursive Hash structure (based on Rails' implementation).
|
18
|
+
* Rack::PostBodyContentTypeParser - Adds support for JSON request bodies. The
|
19
|
+
Rack parameter hash is populated by deserializing the JSON data provided in
|
20
|
+
the request body when the Content-Type is application/json.
|
21
|
+
* Rack::ProcTitle - Displays request information in process title ($0) for
|
22
|
+
monitoring/inspection with ps(1).
|
23
|
+
* Rack::Profiler - Uses ruby-prof to measure request time.
|
24
|
+
* Rack::Sendfile - Enables X-Sendfile support for bodies that can be served
|
25
|
+
from file.
|
26
|
+
* Rack::TimeZone - Detects the clients timezone using JavaScript and sets
|
27
|
+
a variable in Rack's environment with the offset from UTC.
|
28
|
+
* Rack::Evil - Lets the rack application return a response to the client from any place.
|
29
|
+
* Rack::Callbacks - Implements DLS for pure before/after filter like Middlewares.
|
30
|
+
|
31
|
+
=== Use
|
32
|
+
|
33
|
+
Git is the quickest way to the rack-contrib sources:
|
34
|
+
|
35
|
+
git clone git://github.com/rack/rack-contrib.git
|
36
|
+
|
37
|
+
Gems are currently available from GitHub clones:
|
38
|
+
|
39
|
+
gem install rack-rack-contrib --source=http://gems.github.com/
|
40
|
+
|
41
|
+
Requiring 'rack/contrib' will add autoloads to the Rack modules for all of the
|
42
|
+
components included. The following example shows what a simple rackup
|
43
|
+
(+config.ru+) file might look like:
|
44
|
+
|
45
|
+
require 'rack'
|
46
|
+
require 'rack/contrib'
|
47
|
+
|
48
|
+
use Rack::Profiler if ENV['RACK_ENV'] == 'development'
|
49
|
+
|
50
|
+
use Rack::ETag
|
51
|
+
use Rack::MailExceptions
|
52
|
+
|
53
|
+
run theapp
|
54
|
+
|
55
|
+
=== Links
|
56
|
+
|
57
|
+
rack-contrib on GitHub:: <http://github.com/rack/rack-contrib>
|
58
|
+
Rack:: <http://rack.rubyforge.org/>
|
59
|
+
Rack On GitHub:: <http://github.org/rack/rack>
|
60
|
+
rack-devel mailing list:: <http://groups.google.com/group/rack-devel>
|
data/Rakefile
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Rakefile for Rack::Contrib. -*-ruby-*-
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
desc "Run all the tests"
|
6
|
+
task :default => [:test]
|
7
|
+
|
8
|
+
desc "Generate RDox"
|
9
|
+
task "RDOX" do
|
10
|
+
sh "specrb -Ilib:test -a --rdox >RDOX"
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run all the fast tests"
|
14
|
+
task :test do
|
15
|
+
sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Run all the tests"
|
19
|
+
task :fulltest do
|
20
|
+
sh "specrb -Ilib:test -w #{ENV['TEST'] || '-a'} #{ENV['TESTOPTS']}"
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Generate RDoc documentation"
|
24
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
25
|
+
rdoc.options << '--line-numbers' << '--inline-source' <<
|
26
|
+
'--main' << 'README' <<
|
27
|
+
'--title' << 'Rack Contrib Documentation' <<
|
28
|
+
'--charset' << 'utf-8'
|
29
|
+
rdoc.rdoc_dir = "doc"
|
30
|
+
rdoc.rdoc_files.include 'README.rdoc'
|
31
|
+
rdoc.rdoc_files.include 'RDOX'
|
32
|
+
rdoc.rdoc_files.include('lib/rack/*.rb')
|
33
|
+
rdoc.rdoc_files.include('lib/rack/*/*.rb')
|
34
|
+
end
|
35
|
+
task :rdoc => ["RDOX"]
|
36
|
+
|
37
|
+
|
38
|
+
# PACKAGING =================================================================
|
39
|
+
|
40
|
+
# load gemspec like github's gem builder to surface any SAFE issues.
|
41
|
+
require 'rubygems/specification'
|
42
|
+
$spec = eval(File.read('rack-contrib.gemspec'))
|
43
|
+
|
44
|
+
def package(ext='')
|
45
|
+
"pkg/rack-contrib-#{$spec.version}" + ext
|
46
|
+
end
|
47
|
+
|
48
|
+
desc 'Build packages'
|
49
|
+
task :package => %w[.gem .tar.gz].map {|e| package(e)}
|
50
|
+
|
51
|
+
desc 'Build and install as local gem'
|
52
|
+
task :install => package('.gem') do
|
53
|
+
sh "gem install #{package('.gem')}"
|
54
|
+
end
|
55
|
+
|
56
|
+
directory 'pkg/'
|
57
|
+
|
58
|
+
file package('.gem') => %w[pkg/ rack-contrib.gemspec] + $spec.files do |f|
|
59
|
+
sh "gem build rack-contrib.gemspec"
|
60
|
+
mv File.basename(f.name), f.name
|
61
|
+
end
|
62
|
+
|
63
|
+
file package('.tar.gz') => %w[pkg/] + $spec.files do |f|
|
64
|
+
sh "git archive --format=tar HEAD | gzip > #{f.name}"
|
65
|
+
end
|
66
|
+
|
67
|
+
desc 'Publish gem and tarball to rubyforge'
|
68
|
+
task 'publish:gem' => [package('.gem'), package('.tar.gz')] do |t|
|
69
|
+
sh <<-end
|
70
|
+
rubyforge add_release rack rack-contrib #{$spec.version} #{package('.gem')} &&
|
71
|
+
rubyforge add_file rack rack-contrib #{$spec.version} #{package('.tar.gz')}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# GEMSPEC ===================================================================
|
76
|
+
|
77
|
+
file 'rack-contrib.gemspec' => FileList['{lib,test}/**','Rakefile', 'README.rdoc'] do |f|
|
78
|
+
# read spec file and split out manifest section
|
79
|
+
spec = File.read(f.name)
|
80
|
+
parts = spec.split(" # = MANIFEST =\n")
|
81
|
+
fail 'bad spec' if parts.length != 3
|
82
|
+
# determine file list from git ls-files
|
83
|
+
files = `git ls-files`.
|
84
|
+
split("\n").sort.reject{ |file| file =~ /^\./ }.
|
85
|
+
map{ |file| " #{file}" }.join("\n")
|
86
|
+
# piece file back together and write...
|
87
|
+
parts[1] = " s.files = %w[\n#{files}\n ]\n"
|
88
|
+
spec = parts.join(" # = MANIFEST =\n")
|
89
|
+
spec.sub!(/s.date = '.*'/, "s.date = '#{Time.now.strftime("%Y-%m-%d")}'")
|
90
|
+
File.open(f.name, 'w') { |io| io.write(spec) }
|
91
|
+
puts "updated #{f.name}"
|
92
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
# Bounce those annoying favicon.ico requests
|
3
|
+
class BounceFavicon
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
if env["PATH_INFO"] == "/favicon.ico"
|
10
|
+
[404, {"Content-Type" => "text/html", "Content-Length" => "0"}, []]
|
11
|
+
else
|
12
|
+
@app.call(env)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Rack
|
2
|
+
class Callbacks
|
3
|
+
def initialize(&block)
|
4
|
+
@before = []
|
5
|
+
@after = []
|
6
|
+
instance_eval(&block) if block_given?
|
7
|
+
end
|
8
|
+
|
9
|
+
def before(middleware, *args, &block)
|
10
|
+
if block_given?
|
11
|
+
@before << middleware.new(*args, &block)
|
12
|
+
else
|
13
|
+
@before << middleware.new(*args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def after(middleware, *args, &block)
|
18
|
+
if block_given?
|
19
|
+
@after << middleware.new(*args, &block)
|
20
|
+
else
|
21
|
+
@after << middleware.new(*args)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(app)
|
26
|
+
@app = app
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
@before.each {|c| c.call(env) }
|
31
|
+
|
32
|
+
response = @app.call(env)
|
33
|
+
|
34
|
+
@after.inject(response) {|r, c| c.call(r) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
# Automatically sets the ETag header on all String bodies
|
5
|
+
class ETag
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
status, headers, body = @app.call(env)
|
12
|
+
|
13
|
+
if !headers.has_key?('ETag') && body.is_a?(String)
|
14
|
+
headers['ETag'] = %("#{Digest::MD5.hexdigest(body)}")
|
15
|
+
end
|
16
|
+
|
17
|
+
[status, headers, body]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Rack
|
2
|
+
|
3
|
+
# A Rack middleware for providing JSON-P support.
|
4
|
+
#
|
5
|
+
# Full credit to Flinn Mueller (http://actsasflinn.com/) for this contribution.
|
6
|
+
#
|
7
|
+
class JSONP
|
8
|
+
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
# Proxies the request to the application, stripping out the JSON-P callback
|
14
|
+
# method and padding the response with the appropriate callback format.
|
15
|
+
#
|
16
|
+
# Changes nothing if no <tt>callback</tt> param is specified.
|
17
|
+
#
|
18
|
+
def call(env)
|
19
|
+
status, headers, response = @app.call(env)
|
20
|
+
request = Rack::Request.new(env)
|
21
|
+
response = pad(request.params.delete('callback'), response) if request.params.include?('callback')
|
22
|
+
[status, headers, response]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Pads the response with the appropriate callback format according to the
|
26
|
+
# JSON-P spec/requirements.
|
27
|
+
#
|
28
|
+
# The Rack response spec indicates that it should be enumerable. The method
|
29
|
+
# of combining all of the data into a sinle string makes sense since JSON
|
30
|
+
# is returned as a full string.
|
31
|
+
#
|
32
|
+
def pad(callback, response, body = "")
|
33
|
+
response.each{ |s| body << s }
|
34
|
+
"#{callback}(#{body})"
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Rack
|
2
|
+
# Lighttpd sets the wrong SCRIPT_NAME and PATH_INFO if you mount your
|
3
|
+
# FastCGI app at "/". This middleware fixes this issue.
|
4
|
+
|
5
|
+
class LighttpdScriptNameFix
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
env["PATH_INFO"] = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
|
12
|
+
env["SCRIPT_NAME"] = ""
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'i18n'
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
class Locale
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
old_locale = I18n.locale
|
11
|
+
locale = nil
|
12
|
+
|
13
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4
|
14
|
+
if lang = env["HTTP_ACCEPT_LANGUAGE"]
|
15
|
+
lang = lang.split(",").map { |l|
|
16
|
+
l += ';q=1.0' unless l =~ /;q=\d+\.\d+$/
|
17
|
+
l.split(';q=')
|
18
|
+
}.first
|
19
|
+
locale = lang.first.split("-").first
|
20
|
+
else
|
21
|
+
locale = I18n.default_locale
|
22
|
+
end
|
23
|
+
|
24
|
+
locale = env['rack.locale'] = I18n.locale = locale.to_s
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
headers['Content-Language'] = locale
|
27
|
+
I18n.locale = old_locale
|
28
|
+
[status, headers, body]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'net/smtp'
|
2
|
+
require 'tmail'
|
3
|
+
require 'erb'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
# Catches all exceptions raised from the app it wraps and
|
7
|
+
# sends a useful email with the exception, stacktrace, and
|
8
|
+
# contents of the environment.
|
9
|
+
|
10
|
+
class MailExceptions
|
11
|
+
attr_reader :config
|
12
|
+
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
@config = {
|
16
|
+
:to => nil,
|
17
|
+
:from => ENV['USER'] || 'rack',
|
18
|
+
:subject => '[exception] %s',
|
19
|
+
:smtp => {
|
20
|
+
:server => 'localhost',
|
21
|
+
:domain => 'localhost',
|
22
|
+
:port => 25,
|
23
|
+
:authentication => :login,
|
24
|
+
:user_name => nil,
|
25
|
+
:password => nil
|
26
|
+
}
|
27
|
+
}
|
28
|
+
@template = ERB.new(TEMPLATE)
|
29
|
+
yield self if block_given?
|
30
|
+
end
|
31
|
+
|
32
|
+
def call(env)
|
33
|
+
status, headers, body =
|
34
|
+
begin
|
35
|
+
@app.call(env)
|
36
|
+
rescue => boom
|
37
|
+
# TODO don't allow exceptions from send_notification to
|
38
|
+
# propogate
|
39
|
+
send_notification boom, env
|
40
|
+
raise
|
41
|
+
end
|
42
|
+
send_notification env['mail.exception'], env if env['mail.exception']
|
43
|
+
[status, headers, body]
|
44
|
+
end
|
45
|
+
|
46
|
+
%w[to from subject].each do |meth|
|
47
|
+
define_method(meth) { |value| @config[meth.to_sym] = value }
|
48
|
+
end
|
49
|
+
|
50
|
+
def smtp(settings={})
|
51
|
+
@config[:smtp].merge! settings
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def generate_mail(exception, env)
|
56
|
+
mail = TMail::Mail.new
|
57
|
+
mail.to = Array(config[:to])
|
58
|
+
mail.from = config[:from]
|
59
|
+
mail.subject = config[:subject] % [exception.to_s]
|
60
|
+
mail.date = Time.now
|
61
|
+
mail.set_content_type 'text/plain'
|
62
|
+
mail.charset = 'UTF-8'
|
63
|
+
mail.body = @template.result(binding)
|
64
|
+
mail
|
65
|
+
end
|
66
|
+
|
67
|
+
def send_notification(exception, env)
|
68
|
+
mail = generate_mail(exception, env)
|
69
|
+
smtp = config[:smtp]
|
70
|
+
env['mail.sent'] = true
|
71
|
+
return if smtp[:server] == 'example.com'
|
72
|
+
|
73
|
+
Net::SMTP.start smtp[:address], smtp[:port], smtp[:domain], smtp[:user_name], smtp[:password], smtp[:authentication] do |server|
|
74
|
+
mail.to.each do |recipient|
|
75
|
+
server.send_message mail.to_s, mail.from, recipient
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def extract_body(env)
|
81
|
+
if io = env['rack.input']
|
82
|
+
io.rewind if io.respond_to?(:rewind)
|
83
|
+
io.read
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
TEMPLATE = (<<-'EMAIL').gsub(/^ {4}/, '')
|
88
|
+
A <%= exception.class.to_s %> occured: <%= exception.to_s %>
|
89
|
+
<% if body = extract_body(env) %>
|
90
|
+
|
91
|
+
===================================================================
|
92
|
+
Request Body:
|
93
|
+
===================================================================
|
94
|
+
|
95
|
+
<%= body.gsub(/^/, ' ') %>
|
96
|
+
<% end %>
|
97
|
+
|
98
|
+
===================================================================
|
99
|
+
Rack Environment:
|
100
|
+
===================================================================
|
101
|
+
|
102
|
+
PID: <%= $$ %>
|
103
|
+
PWD: <%= Dir.getwd %>
|
104
|
+
|
105
|
+
<%= env.to_a.
|
106
|
+
sort{|a,b| a.first <=> b.first}.
|
107
|
+
map{ |k,v| "%-25s%p" % [k+':', v] }.
|
108
|
+
join("\n ") %>
|
109
|
+
|
110
|
+
<% if exception.respond_to?(:backtrace) %>
|
111
|
+
===================================================================
|
112
|
+
Backtrace:
|
113
|
+
===================================================================
|
114
|
+
|
115
|
+
<%= exception.backtrace.join("\n ") %>
|
116
|
+
<% end %>
|
117
|
+
EMAIL
|
118
|
+
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'strscan'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
# Rack middleware for parsing POST/PUT body data into nested parameters
|
6
|
+
class NestedParams
|
7
|
+
|
8
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
9
|
+
POST_BODY = 'rack.input'.freeze
|
10
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
11
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
12
|
+
FORM_VARS = 'rack.request.form_vars'.freeze
|
13
|
+
|
14
|
+
# supported content type
|
15
|
+
URL_ENCODED = 'application/x-www-form-urlencoded'.freeze
|
16
|
+
|
17
|
+
def initialize(app)
|
18
|
+
@app = app
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
if form_vars = env[FORM_VARS]
|
23
|
+
env[FORM_HASH] = parse_query_parameters(form_vars)
|
24
|
+
elsif env[CONTENT_TYPE] == URL_ENCODED
|
25
|
+
post_body = env[POST_BODY]
|
26
|
+
env[FORM_INPUT] = post_body
|
27
|
+
env[FORM_HASH] = parse_query_parameters(post_body.read)
|
28
|
+
post_body.rewind if post_body.respond_to?(:rewind)
|
29
|
+
end
|
30
|
+
@app.call(env)
|
31
|
+
end
|
32
|
+
|
33
|
+
## the rest is nabbed from Rails ##
|
34
|
+
|
35
|
+
def parse_query_parameters(query_string)
|
36
|
+
return {} if query_string.nil? or query_string.empty?
|
37
|
+
|
38
|
+
pairs = query_string.split('&').collect do |chunk|
|
39
|
+
next if chunk.empty?
|
40
|
+
key, value = chunk.split('=', 2)
|
41
|
+
next if key.empty?
|
42
|
+
value = value.nil? ? nil : CGI.unescape(value)
|
43
|
+
[ CGI.unescape(key), value ]
|
44
|
+
end.compact
|
45
|
+
|
46
|
+
UrlEncodedPairParser.new(pairs).result
|
47
|
+
end
|
48
|
+
|
49
|
+
class UrlEncodedPairParser < StringScanner
|
50
|
+
attr_reader :top, :parent, :result
|
51
|
+
|
52
|
+
def initialize(pairs = [])
|
53
|
+
super('')
|
54
|
+
@result = {}
|
55
|
+
pairs.each { |key, value| parse(key, value) }
|
56
|
+
end
|
57
|
+
|
58
|
+
KEY_REGEXP = %r{([^\[\]=&]+)}
|
59
|
+
BRACKETED_KEY_REGEXP = %r{\[([^\[\]=&]+)\]}
|
60
|
+
|
61
|
+
# Parse the query string
|
62
|
+
def parse(key, value)
|
63
|
+
self.string = key
|
64
|
+
@top, @parent = result, nil
|
65
|
+
|
66
|
+
# First scan the bare key
|
67
|
+
key = scan(KEY_REGEXP) or return
|
68
|
+
key = post_key_check(key)
|
69
|
+
|
70
|
+
# Then scan as many nestings as present
|
71
|
+
until eos?
|
72
|
+
r = scan(BRACKETED_KEY_REGEXP) or return
|
73
|
+
key = self[1]
|
74
|
+
key = post_key_check(key)
|
75
|
+
end
|
76
|
+
|
77
|
+
bind(key, value)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
# After we see a key, we must look ahead to determine our next action. Cases:
|
82
|
+
#
|
83
|
+
# [] follows the key. Then the value must be an array.
|
84
|
+
# = follows the key. (A value comes next)
|
85
|
+
# & or the end of string follows the key. Then the key is a flag.
|
86
|
+
# otherwise, a hash follows the key.
|
87
|
+
def post_key_check(key)
|
88
|
+
if scan(/\[\]/) # a[b][] indicates that b is an array
|
89
|
+
container(key, Array)
|
90
|
+
nil
|
91
|
+
elsif check(/\[[^\]]/) # a[b] indicates that a is a hash
|
92
|
+
container(key, Hash)
|
93
|
+
nil
|
94
|
+
else # End of key? We do nothing.
|
95
|
+
key
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Add a container to the stack.
|
100
|
+
def container(key, klass)
|
101
|
+
type_conflict! klass, top[key] if top.is_a?(Hash) && top.key?(key) && ! top[key].is_a?(klass)
|
102
|
+
value = bind(key, klass.new)
|
103
|
+
type_conflict! klass, value unless value.is_a?(klass)
|
104
|
+
push(value)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Push a value onto the 'stack', which is actually only the top 2 items.
|
108
|
+
def push(value)
|
109
|
+
@parent, @top = @top, value
|
110
|
+
end
|
111
|
+
|
112
|
+
# Bind a key (which may be nil for items in an array) to the provided value.
|
113
|
+
def bind(key, value)
|
114
|
+
if top.is_a? Array
|
115
|
+
if key
|
116
|
+
if top[-1].is_a?(Hash) && ! top[-1].key?(key)
|
117
|
+
top[-1][key] = value
|
118
|
+
else
|
119
|
+
top << {key => value}
|
120
|
+
end
|
121
|
+
push top.last
|
122
|
+
return top[key]
|
123
|
+
else
|
124
|
+
top << value
|
125
|
+
return value
|
126
|
+
end
|
127
|
+
elsif top.is_a? Hash
|
128
|
+
key = CGI.unescape(key)
|
129
|
+
parent << (@top = {}) if top.key?(key) && parent.is_a?(Array)
|
130
|
+
top[key] ||= value
|
131
|
+
return top[key]
|
132
|
+
else
|
133
|
+
raise ArgumentError, "Don't know what to do: top is #{top.inspect}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def type_conflict!(klass, value)
|
138
|
+
raise TypeError, "Conflicting types for parameter containers. Expected an instance of #{klass} but found an instance of #{value.class}. This can be caused by colliding Array and Hash parameters like qs[]=value&qs[key]=value. (The parameters received were #{value.inspect}.)"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
begin
|
2
|
+
require 'json'
|
3
|
+
rescue LoadError => e
|
4
|
+
require 'json/pure'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Rack
|
8
|
+
|
9
|
+
# A Rack middleware for parsing POST/PUT body data when Content-Type is
|
10
|
+
# not one of the standard supported types, like <tt>application/json</tt>.
|
11
|
+
#
|
12
|
+
# TODO: Find a better name.
|
13
|
+
#
|
14
|
+
class PostBodyContentTypeParser
|
15
|
+
|
16
|
+
# Constants
|
17
|
+
#
|
18
|
+
CONTENT_TYPE = 'CONTENT_TYPE'.freeze
|
19
|
+
POST_BODY = 'rack.input'.freeze
|
20
|
+
FORM_INPUT = 'rack.request.form_input'.freeze
|
21
|
+
FORM_HASH = 'rack.request.form_hash'.freeze
|
22
|
+
|
23
|
+
# Supported Content-Types
|
24
|
+
#
|
25
|
+
APPLICATION_JSON = 'application/json'.freeze
|
26
|
+
|
27
|
+
def initialize(app)
|
28
|
+
@app = app
|
29
|
+
end
|
30
|
+
|
31
|
+
def call(env)
|
32
|
+
case env[CONTENT_TYPE]
|
33
|
+
when APPLICATION_JSON
|
34
|
+
env.update(FORM_HASH => JSON.parse(env[POST_BODY].read), FORM_INPUT => env[POST_BODY])
|
35
|
+
end
|
36
|
+
@app.call(env)
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|