tennpipes-base 3.6.6
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.
- checksums.yaml +7 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +294 -0
- data/Rakefile +1 -0
- data/bin/tennpipes +8 -0
- data/lib/tennpipes-base.rb +196 -0
- data/lib/tennpipes-base/application.rb +175 -0
- data/lib/tennpipes-base/application/application_setup.rb +202 -0
- data/lib/tennpipes-base/application/authenticity_token.rb +25 -0
- data/lib/tennpipes-base/application/flash.rb +229 -0
- data/lib/tennpipes-base/application/params_protection.rb +129 -0
- data/lib/tennpipes-base/application/routing.rb +1002 -0
- data/lib/tennpipes-base/application/show_exceptions.rb +50 -0
- data/lib/tennpipes-base/caller.rb +53 -0
- data/lib/tennpipes-base/cli/adapter.rb +33 -0
- data/lib/tennpipes-base/cli/base.rb +105 -0
- data/lib/tennpipes-base/cli/console.rb +20 -0
- data/lib/tennpipes-base/cli/launcher.rb +103 -0
- data/lib/tennpipes-base/cli/rake.rb +50 -0
- data/lib/tennpipes-base/cli/rake_tasks.rb +72 -0
- data/lib/tennpipes-base/command.rb +38 -0
- data/lib/tennpipes-base/ext/sinatra.rb +29 -0
- data/lib/tennpipes-base/filter.rb +52 -0
- data/lib/tennpipes-base/images/404.png +0 -0
- data/lib/tennpipes-base/images/500.png +0 -0
- data/lib/tennpipes-base/loader.rb +202 -0
- data/lib/tennpipes-base/logger.rb +492 -0
- data/lib/tennpipes-base/module.rb +58 -0
- data/lib/tennpipes-base/mounter.rb +308 -0
- data/lib/tennpipes-base/path_router.rb +119 -0
- data/lib/tennpipes-base/path_router/compiler.rb +110 -0
- data/lib/tennpipes-base/path_router/error_handler.rb +8 -0
- data/lib/tennpipes-base/path_router/matcher.rb +123 -0
- data/lib/tennpipes-base/path_router/route.rb +169 -0
- data/lib/tennpipes-base/reloader.rb +309 -0
- data/lib/tennpipes-base/reloader/rack.rb +26 -0
- data/lib/tennpipes-base/reloader/storage.rb +55 -0
- data/lib/tennpipes-base/router.rb +98 -0
- data/lib/tennpipes-base/server.rb +119 -0
- data/lib/tennpipes-base/tasks.rb +21 -0
- data/lib/tennpipes-base/version.rb +20 -0
- data/lib/tennpipes-base/version.rb~ +20 -0
- data/test/fixtures/app_gem/Gemfile +4 -0
- data/test/fixtures/app_gem/app/app.rb +3 -0
- data/test/fixtures/app_gem/app_gem.gemspec +17 -0
- data/test/fixtures/app_gem/lib/app_gem.rb +7 -0
- data/test/fixtures/app_gem/lib/app_gem/version.rb +3 -0
- data/test/fixtures/apps/complex.rb +32 -0
- data/test/fixtures/apps/demo_app.rb +7 -0
- data/test/fixtures/apps/demo_demo.rb +7 -0
- data/test/fixtures/apps/demo_project/api/app.rb +7 -0
- data/test/fixtures/apps/demo_project/api/lib/api_lib.rb +3 -0
- data/test/fixtures/apps/demo_project/app.rb +7 -0
- data/test/fixtures/apps/external_apps/fake_lib.rb +1 -0
- data/test/fixtures/apps/external_apps/fake_root.rb +2 -0
- data/test/fixtures/apps/helpers/class_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/instance_methods_helpers.rb +4 -0
- data/test/fixtures/apps/helpers/support.rb +1 -0
- data/test/fixtures/apps/helpers/system_helpers.rb +8 -0
- data/test/fixtures/apps/kiq.rb +3 -0
- data/test/fixtures/apps/lib/myklass.rb +2 -0
- data/test/fixtures/apps/lib/myklass/mysubklass.rb +4 -0
- data/test/fixtures/apps/models/child.rb +2 -0
- data/test/fixtures/apps/models/parent.rb +5 -0
- data/test/fixtures/apps/mountable_apps/rack_apps.rb +15 -0
- data/test/fixtures/apps/mountable_apps/static.html +1 -0
- data/test/fixtures/apps/precompiled_app.rb +19 -0
- data/test/fixtures/apps/simple.rb +32 -0
- data/test/fixtures/apps/static.rb +10 -0
- data/test/fixtures/apps/system.rb +13 -0
- data/test/fixtures/apps/system_class_methods_demo.rb +7 -0
- data/test/fixtures/apps/system_instance_methods_demo.rb +7 -0
- data/test/fixtures/dependencies/a.rb +9 -0
- data/test/fixtures/dependencies/b.rb +4 -0
- data/test/fixtures/dependencies/c.rb +1 -0
- data/test/fixtures/dependencies/circular/e.rb +13 -0
- data/test/fixtures/dependencies/circular/f.rb +2 -0
- data/test/fixtures/dependencies/circular/g.rb +2 -0
- data/test/fixtures/dependencies/d.rb +4 -0
- data/test/fixtures/reloadable_apps/external/app/app.rb +6 -0
- data/test/fixtures/reloadable_apps/external/app/controllers/base.rb +6 -0
- data/test/fixtures/reloadable_apps/main/app.rb +10 -0
- data/test/helper.rb +30 -0
- data/test/test_application.rb +185 -0
- data/test/test_core.rb +93 -0
- data/test/test_csrf_protection.rb +208 -0
- data/test/test_dependencies.rb +57 -0
- data/test/test_filters.rb +389 -0
- data/test/test_flash.rb +168 -0
- data/test/test_locale.rb +21 -0
- data/test/test_logger.rb +295 -0
- data/test/test_mounter.rb +302 -0
- data/test/test_params_protection.rb +195 -0
- data/test/test_reloader_complex.rb +74 -0
- data/test/test_reloader_external.rb +21 -0
- data/test/test_reloader_simple.rb +101 -0
- data/test/test_reloader_system.rb +113 -0
- data/test/test_restful_routing.rb +33 -0
- data/test/test_router.rb +281 -0
- data/test/test_routing.rb +2328 -0
- metadata +301 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'active_support/core_ext/object/deep_dup' # AS 4.1
|
|
3
|
+
rescue LoadError
|
|
4
|
+
require 'active_support/core_ext/hash/deep_dup' # AS >= 3.1
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
module Tennpipes
|
|
8
|
+
##
|
|
9
|
+
# Tennpipes application module providing means for mass-assignment protection.
|
|
10
|
+
#
|
|
11
|
+
module ParamsProtection
|
|
12
|
+
class << self
|
|
13
|
+
def registered(app)
|
|
14
|
+
included(app)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def included(base)
|
|
18
|
+
base.send(:include, InstanceMethods)
|
|
19
|
+
base.extend(ClassMethods)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module ClassMethods
|
|
24
|
+
##
|
|
25
|
+
# Implements filtering of url query params. Can prevent mass-assignment.
|
|
26
|
+
#
|
|
27
|
+
# @example
|
|
28
|
+
# post :update, :params => [:name, :email]
|
|
29
|
+
# post :update, :params => [:name, :id => Integer]
|
|
30
|
+
# post :update, :params => [:name => proc{ |v| v.reverse }]
|
|
31
|
+
# post :update, :params => [:name, :parent => [:name, :position]]
|
|
32
|
+
# post :update, :params => false
|
|
33
|
+
# post :update, :params => true
|
|
34
|
+
# @example
|
|
35
|
+
# params :name, :email, :password => prox{ |v| v.reverse }
|
|
36
|
+
# post :update
|
|
37
|
+
# @example
|
|
38
|
+
# App.controller :accounts, :params => [:name, :position] do
|
|
39
|
+
# post :create
|
|
40
|
+
# post :update, :with => [ :id ], :params => [:name, :position, :addition]
|
|
41
|
+
# get :show, :with => :id, :params => false
|
|
42
|
+
# get :search, :params => true
|
|
43
|
+
# end
|
|
44
|
+
#
|
|
45
|
+
def params(*allowed_params)
|
|
46
|
+
allowed_params = prepare_allowed_params(allowed_params)
|
|
47
|
+
condition do
|
|
48
|
+
@original_params = params.deep_dup
|
|
49
|
+
filter_params!(params, allowed_params)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def prepare_allowed_params(allowed_params)
|
|
56
|
+
param_filter = {}
|
|
57
|
+
allowed_params.each do |key,value|
|
|
58
|
+
case
|
|
59
|
+
when key.kind_of?(Hash) && !value
|
|
60
|
+
param_filter.update(prepare_allowed_params(key))
|
|
61
|
+
when value.kind_of?(Hash) || value.kind_of?(Array)
|
|
62
|
+
param_filter[key.to_s] = prepare_allowed_params(value)
|
|
63
|
+
else
|
|
64
|
+
param_filter[key.to_s] = value == false ? false : (value || true)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
param_filter.freeze
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
module InstanceMethods
|
|
72
|
+
##
|
|
73
|
+
# Filters a hash of parameters leaving only allowed ones and possibly
|
|
74
|
+
# typecasting and processing the others.
|
|
75
|
+
#
|
|
76
|
+
# @param [Hash] params
|
|
77
|
+
# Parameters to filter.
|
|
78
|
+
# Warning: this hash will be changed by deleting or replacing its values.
|
|
79
|
+
# @param [Hash] allowed_params
|
|
80
|
+
# A hash of allowed keys and value classes or processing procs. Supported
|
|
81
|
+
# scalar classes are: Integer (empty string is cast to nil).
|
|
82
|
+
#
|
|
83
|
+
# @example
|
|
84
|
+
# filter_params!( { "a" => "1", "b" => "abc", "d" => "drop" },
|
|
85
|
+
# { "a" => Integer, "b" => true } )
|
|
86
|
+
# # => { "a" => 1, "b" => "abc" }
|
|
87
|
+
# filter_params!( { "id" => "", "child" => { "name" => "manny" } },
|
|
88
|
+
# { "id" => Integer, "child" => { "name" => proc{ |v| v.camelize } } } )
|
|
89
|
+
# # => { "id" => nil, "child" => { "name" => "Manny" } }
|
|
90
|
+
# filter_params!( { "a" => ["1", "2", "3"] },
|
|
91
|
+
# { "a" => true } )
|
|
92
|
+
# # => { "a" => ["1", "2", "3"] }
|
|
93
|
+
# filter_params!( { "persons" => {"p-1" => { "name" => "manny", "age" => "50" }, "p-2" => { "name" => "richard", "age" => "50" } } },
|
|
94
|
+
# { "persons" => { "name" => true } } )
|
|
95
|
+
# # => { "persons" => {"p-1" => { "name" => "manny" }, "p-2" => { "name" => "richard" } } }
|
|
96
|
+
#
|
|
97
|
+
def filter_params!(params, allowed_params)
|
|
98
|
+
params.each do |key,value|
|
|
99
|
+
type = allowed_params[key]
|
|
100
|
+
next if value.kind_of?(Array) && type
|
|
101
|
+
case
|
|
102
|
+
when type.kind_of?(Hash) && value.kind_of?(Hash)
|
|
103
|
+
if key == key.pluralize && value.values.first.kind_of?(Hash)
|
|
104
|
+
value.each do |array_index,array_value|
|
|
105
|
+
value[array_index] = filter_params!(array_value, type)
|
|
106
|
+
end
|
|
107
|
+
else
|
|
108
|
+
params[key] = filter_params!(value, type)
|
|
109
|
+
end
|
|
110
|
+
when type == Integer
|
|
111
|
+
params[key] = value.empty? ? nil : value.to_i
|
|
112
|
+
when type.kind_of?(Proc)
|
|
113
|
+
params[key] = type.call(value)
|
|
114
|
+
when type == true
|
|
115
|
+
else
|
|
116
|
+
params.delete(key)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
##
|
|
122
|
+
# Returns the original unfiltered query parameters hash.
|
|
123
|
+
#
|
|
124
|
+
def original_params
|
|
125
|
+
@original_params || params
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,1002 @@
|
|
|
1
|
+
require 'tennpipes-assist'
|
|
2
|
+
require 'tennpipes-base/path_router' unless defined?(PathRouter)
|
|
3
|
+
require 'tennpipes-base/ext/sinatra'
|
|
4
|
+
require 'tennpipes-base/filter'
|
|
5
|
+
|
|
6
|
+
module Tennpipes
|
|
7
|
+
##
|
|
8
|
+
# Tennpipes provides advanced routing definition support to make routes and
|
|
9
|
+
# url generation much easier. This routing system supports named route
|
|
10
|
+
# aliases and easy access to url paths. The benefits of this is that instead
|
|
11
|
+
# of having to hard-code route urls into every area of your application, now
|
|
12
|
+
# we can just define the urls in a single spot and then attach an alias
|
|
13
|
+
# which can be used to refer to the url throughout the application.
|
|
14
|
+
#
|
|
15
|
+
module Routing
|
|
16
|
+
# Defines common content-type alias mappings.
|
|
17
|
+
CONTENT_TYPE_ALIASES = { :htm => :html } unless defined?(CONTENT_TYPE_ALIASES)
|
|
18
|
+
# Defines the available route priorities supporting route deferrals.
|
|
19
|
+
ROUTE_PRIORITY = {:high => 0, :normal => 1, :low => 2} unless defined?(ROUTE_PRIORITY)
|
|
20
|
+
|
|
21
|
+
# Raised when a route was invalid or cannot be processed.
|
|
22
|
+
class UnrecognizedException < RuntimeError; end
|
|
23
|
+
|
|
24
|
+
# Raised when block arity was nonzero and was not same with
|
|
25
|
+
# captured parameter length.
|
|
26
|
+
class BlockArityError < ArgumentError
|
|
27
|
+
def initialize(path, block_arity, required_arity)
|
|
28
|
+
super "route block arity does not match path '#{path}' (#{block_arity} for #{required_arity})"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class Parent < String
|
|
33
|
+
attr_reader :map
|
|
34
|
+
attr_reader :optional
|
|
35
|
+
attr_reader :options
|
|
36
|
+
|
|
37
|
+
alias_method :optional?, :optional
|
|
38
|
+
|
|
39
|
+
def initialize(value, options={})
|
|
40
|
+
super(value.to_s)
|
|
41
|
+
@map = options.delete(:map)
|
|
42
|
+
@optional = options.delete(:optional)
|
|
43
|
+
@options = options
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class << self
|
|
48
|
+
##
|
|
49
|
+
# Main class that register this extension.
|
|
50
|
+
#
|
|
51
|
+
def registered(app)
|
|
52
|
+
app.send(:include, InstanceMethods)
|
|
53
|
+
app.extend(ClassMethods)
|
|
54
|
+
end
|
|
55
|
+
alias :included :registered
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Class methods responsible for enhanced routing for controllers.
|
|
59
|
+
module ClassMethods
|
|
60
|
+
##
|
|
61
|
+
# Method to organize our routes in a better way.
|
|
62
|
+
#
|
|
63
|
+
# @param [Array] args
|
|
64
|
+
# Controller arguments.
|
|
65
|
+
#
|
|
66
|
+
# @yield []
|
|
67
|
+
# The given block will be used to define the routes within the
|
|
68
|
+
# Controller.
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# controller :admin do
|
|
72
|
+
# get :index do; ...; end
|
|
73
|
+
# get :show, :with => :id do; ...; end
|
|
74
|
+
# end
|
|
75
|
+
#
|
|
76
|
+
# url(:admin_index) # => "/admin"
|
|
77
|
+
# url(:admin_show, :id => 1) # "/admin/show/1"
|
|
78
|
+
#
|
|
79
|
+
# @example Using named routes follow the sinatra way:
|
|
80
|
+
# controller "/admin" do
|
|
81
|
+
# get "/index" do; ...; end
|
|
82
|
+
# get "/show/:id" do; ...; end
|
|
83
|
+
# end
|
|
84
|
+
#
|
|
85
|
+
# @example Supply +:provides+ to all controller routes:
|
|
86
|
+
# controller :provides => [:html, :xml, :json] do
|
|
87
|
+
# get :index do; "respond to html, xml and json"; end
|
|
88
|
+
# post :index do; "respond to html, xml and json"; end
|
|
89
|
+
# get :foo do; "respond to html, xml and json"; end
|
|
90
|
+
# end
|
|
91
|
+
#
|
|
92
|
+
# @example Specify parent resources in tennpipes with the +:parent+ option on the controller:
|
|
93
|
+
# controllers :product, :parent => :user do
|
|
94
|
+
# get :index do
|
|
95
|
+
# # url is generated as "/user/#{params[:user_id]}/product"
|
|
96
|
+
# # url_for(:product, :index, :user_id => 5) => "/user/5/product"
|
|
97
|
+
# end
|
|
98
|
+
# get :show, :with => :id do
|
|
99
|
+
# # url is generated as "/user/#{params[:user_id]}/product/show/#{params[:id]}"
|
|
100
|
+
# # url_for(:product, :show, :user_id => 5, :id => 10) => "/user/5/product/show/10"
|
|
101
|
+
# end
|
|
102
|
+
# end
|
|
103
|
+
#
|
|
104
|
+
# @example Specify conditions to run for all routes:
|
|
105
|
+
# controller :conditions => {:protect => true} do
|
|
106
|
+
# def self.protect(protected)
|
|
107
|
+
# condition do
|
|
108
|
+
# halt 403, "No secrets for you!" unless params[:key] == "s3cr3t"
|
|
109
|
+
# end if protected
|
|
110
|
+
# end
|
|
111
|
+
#
|
|
112
|
+
# # This route will only return "secret stuff" if the user goes to
|
|
113
|
+
# # `/private?key=s3cr3t`.
|
|
114
|
+
# get("/private") { "secret stuff" }
|
|
115
|
+
#
|
|
116
|
+
# # And this one, too!
|
|
117
|
+
# get("/also-private") { "secret stuff" }
|
|
118
|
+
#
|
|
119
|
+
# # But you can override the conditions for each route as needed.
|
|
120
|
+
# # This route will be publicly accessible without providing the
|
|
121
|
+
# # secret key.
|
|
122
|
+
# get :index, :protect => false do
|
|
123
|
+
# "Welcome!"
|
|
124
|
+
# end
|
|
125
|
+
# end
|
|
126
|
+
#
|
|
127
|
+
# @example Supply default values:
|
|
128
|
+
# controller :lang => :de do
|
|
129
|
+
# get :index, :map => "/:lang" do; "params[:lang] == :de"; end
|
|
130
|
+
# end
|
|
131
|
+
#
|
|
132
|
+
# In a controller, before and after filters are scoped and don't
|
|
133
|
+
# affect other controllers or the main app.
|
|
134
|
+
# In a controller, layouts are scoped and don't affect other
|
|
135
|
+
# controllers or the main app.
|
|
136
|
+
#
|
|
137
|
+
# @example
|
|
138
|
+
# controller :posts do
|
|
139
|
+
# layout :post
|
|
140
|
+
# before { foo }
|
|
141
|
+
# after { bar }
|
|
142
|
+
# end
|
|
143
|
+
#
|
|
144
|
+
def controller(*args, &block)
|
|
145
|
+
if block_given?
|
|
146
|
+
with_new_options(*args) { instance_eval(&block) }
|
|
147
|
+
else
|
|
148
|
+
include(*args) if extensions.any?
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
alias :controllers :controller
|
|
152
|
+
|
|
153
|
+
##
|
|
154
|
+
# Add a before filter hook.
|
|
155
|
+
#
|
|
156
|
+
# @see #construct_filter
|
|
157
|
+
#
|
|
158
|
+
def before(*args, &block)
|
|
159
|
+
add_filter :before, &(args.empty? ? block : construct_filter(*args, &block))
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
##
|
|
163
|
+
# Add an after filter hook.
|
|
164
|
+
#
|
|
165
|
+
# @see #construct_filter
|
|
166
|
+
#
|
|
167
|
+
def after(*args, &block)
|
|
168
|
+
add_filter :after, &(args.empty? ? block : construct_filter(*args, &block))
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
##
|
|
172
|
+
# Adds a filter hook to a request.
|
|
173
|
+
#
|
|
174
|
+
def add_filter(type, &block)
|
|
175
|
+
filters[type] << block
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
##
|
|
179
|
+
# Creates a filter to process before/after the matching route.
|
|
180
|
+
#
|
|
181
|
+
# @param [Array] args
|
|
182
|
+
#
|
|
183
|
+
# @example We are be able to filter with String path
|
|
184
|
+
# before('/') { 'only to :index' }
|
|
185
|
+
# get(:index} { 'foo' } # => filter match only before this.
|
|
186
|
+
# get(:main) { 'bar' }
|
|
187
|
+
#
|
|
188
|
+
# @example is the same of
|
|
189
|
+
# before(:index) { 'only to :index' }
|
|
190
|
+
# get(:index} { 'foo' } # => filter match only before this.
|
|
191
|
+
# get(:main) { 'bar' }
|
|
192
|
+
#
|
|
193
|
+
# @example it works only for the given controller
|
|
194
|
+
# controller :foo do
|
|
195
|
+
# before(:index) { 'only to for :foo_index' }
|
|
196
|
+
# get(:index} { 'foo' } # => filter match only before this.
|
|
197
|
+
# get(:main) { 'bar' }
|
|
198
|
+
# end
|
|
199
|
+
#
|
|
200
|
+
# controller :bar do
|
|
201
|
+
# before(:index) { 'only to for :bar_index' }
|
|
202
|
+
# get(:index} { 'foo' } # => filter match only before this.
|
|
203
|
+
# get(:main) { 'bar' }
|
|
204
|
+
# end
|
|
205
|
+
#
|
|
206
|
+
# @example if filters based on a symbol or regexp
|
|
207
|
+
# before :index, /main/ do; ... end
|
|
208
|
+
# # => match only path that are +/+ or contains +main+
|
|
209
|
+
#
|
|
210
|
+
# @example filtering everything except an occurrence
|
|
211
|
+
# before :except => :index do; ...; end
|
|
212
|
+
#
|
|
213
|
+
# @example you can also filter using a request param
|
|
214
|
+
# before :agent => /IE/ do; ...; end
|
|
215
|
+
# # => match +HTTP_USER_AGENT+ containing +IE+
|
|
216
|
+
#
|
|
217
|
+
# @see http://www.tennpipesrb.com/guides/controllers#route-filters
|
|
218
|
+
#
|
|
219
|
+
def construct_filter(*args, &block)
|
|
220
|
+
options = args.extract_options!
|
|
221
|
+
if except = options.delete(:except)
|
|
222
|
+
fail "You cannot use :except with other options specified" unless args.empty? && options.empty?
|
|
223
|
+
options = Array(except).extract_options!
|
|
224
|
+
end
|
|
225
|
+
Filter.new(!except, @_controller, options, Array(except || args), &block)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
##
|
|
229
|
+
# Provides many parents with shallowing.
|
|
230
|
+
#
|
|
231
|
+
# @param [Symbol] name
|
|
232
|
+
# The parent name.
|
|
233
|
+
#
|
|
234
|
+
# @param [Hash] options
|
|
235
|
+
# Additional options.
|
|
236
|
+
#
|
|
237
|
+
# @example
|
|
238
|
+
# controllers :product do
|
|
239
|
+
# parent :shop, :optional => true, :map => "/my/stand"
|
|
240
|
+
# parent :category, :optional => true
|
|
241
|
+
# get :show, :with => :id do
|
|
242
|
+
# # generated urls:
|
|
243
|
+
# # "/product/show/#{params[:id]}"
|
|
244
|
+
# # "/my/stand/#{params[:shop_id]}/product/show/#{params[:id]}"
|
|
245
|
+
# # "/my/stand/#{params[:shop_id]}/category/#{params[:category_id]}/product/show/#{params[:id]}"
|
|
246
|
+
# # url_for(:product, :show, :id => 10) => "/product/show/10"
|
|
247
|
+
# # url_for(:product, :show, :shop_id => 5, :id => 10) => "/my/stand/5/product/show/10"
|
|
248
|
+
# # url_for(:product, :show, :shop_id => 5, :category_id => 1, :id => 10) => "/my/stand/5/category/1/product/show/10"
|
|
249
|
+
# end
|
|
250
|
+
# end
|
|
251
|
+
#
|
|
252
|
+
def parent(name = nil, options={})
|
|
253
|
+
return super() unless name
|
|
254
|
+
defaults = { :optional => false, :map => name.to_s }
|
|
255
|
+
options = defaults.merge(options)
|
|
256
|
+
@_parent = Array(@_parent) unless @_parent.is_a?(Array)
|
|
257
|
+
@_parent << Parent.new(name, options)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
##
|
|
261
|
+
# Using PathRouter, for features and configurations.
|
|
262
|
+
#
|
|
263
|
+
# @example
|
|
264
|
+
# router.add('/greedy/:greed')
|
|
265
|
+
# router.recognize('/simple')
|
|
266
|
+
#
|
|
267
|
+
def router
|
|
268
|
+
@router ||= PathRouter.new
|
|
269
|
+
block_given? ? yield(@router) : @router
|
|
270
|
+
end
|
|
271
|
+
alias :urls :router
|
|
272
|
+
|
|
273
|
+
def compiled_router
|
|
274
|
+
if @deferred_routes
|
|
275
|
+
deferred_routes.each do |routes|
|
|
276
|
+
routes.each do |(route, dest)|
|
|
277
|
+
route.to(&dest)
|
|
278
|
+
route.before_filters.flatten!
|
|
279
|
+
route.after_filters.flatten!
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
@deferred_routes = nil
|
|
283
|
+
end
|
|
284
|
+
router
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def deferred_routes
|
|
288
|
+
@deferred_routes ||= ROUTE_PRIORITY.map{[]}
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def reset_router!
|
|
292
|
+
@deferred_routes = nil
|
|
293
|
+
router.reset!
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
##
|
|
297
|
+
# Recognize a given path.
|
|
298
|
+
#
|
|
299
|
+
# @param [String] path
|
|
300
|
+
# Path+Query to parse
|
|
301
|
+
#
|
|
302
|
+
# @return [Symbol, Hash]
|
|
303
|
+
# Returns controller and query params.
|
|
304
|
+
#
|
|
305
|
+
# @example Giving a controller like:
|
|
306
|
+
# controller :foo do
|
|
307
|
+
# get :bar, :map => 'foo-bar-:id'; ...; end
|
|
308
|
+
# end
|
|
309
|
+
#
|
|
310
|
+
# @example You should be able to reverse:
|
|
311
|
+
# MyApp.url(:foo_bar, :id => :mine)
|
|
312
|
+
# # => /foo-bar-mine
|
|
313
|
+
#
|
|
314
|
+
# @example Into this:
|
|
315
|
+
# MyApp.recognize_path('foo-bar-mine')
|
|
316
|
+
# # => [:foo_bar, :id => :mine]
|
|
317
|
+
#
|
|
318
|
+
def recognize_path(path)
|
|
319
|
+
responses = @router.recognize_path(path)
|
|
320
|
+
[responses[0], responses[1]]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
##
|
|
324
|
+
# Instance method for url generation.
|
|
325
|
+
#
|
|
326
|
+
# @option options [String] :fragment
|
|
327
|
+
# An addition to url to identify a portion of requested resource (i.e #something).
|
|
328
|
+
# @option options [String] :anchor
|
|
329
|
+
# Synonym for fragment.
|
|
330
|
+
#
|
|
331
|
+
# @example
|
|
332
|
+
# url(:show, :id => 1)
|
|
333
|
+
# url(:show, :name => 'test', :id => 24)
|
|
334
|
+
# url(:show, 1)
|
|
335
|
+
# url(:controller_name, :show, :id => 21)
|
|
336
|
+
# url(:controller_show, :id => 29)
|
|
337
|
+
# url(:index, :fragment => 'comments')
|
|
338
|
+
#
|
|
339
|
+
def url(*args)
|
|
340
|
+
params = args.extract_options!
|
|
341
|
+
fragment = params.delete(:fragment) || params.delete(:anchor)
|
|
342
|
+
path = make_path_with_params(args, value_to_param(params.symbolize_keys))
|
|
343
|
+
rebase_url(fragment ? path << '#' << fragment.to_s : path)
|
|
344
|
+
end
|
|
345
|
+
alias :url_for :url
|
|
346
|
+
|
|
347
|
+
def get(path, *args, &block)
|
|
348
|
+
conditions = @conditions.dup
|
|
349
|
+
route('GET', path, *args, &block)
|
|
350
|
+
|
|
351
|
+
@conditions = conditions
|
|
352
|
+
route('HEAD', path, *args, &block)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def put(path, *args, &block) route 'PUT', path, *args, &block end
|
|
356
|
+
def post(path, *args, &block) route 'POST', path, *args, &block end
|
|
357
|
+
def delete(path, *args, &block) route 'DELETE', path, *args, &block end
|
|
358
|
+
def head(path, *args, &block) route 'HEAD', path, *args, &block end
|
|
359
|
+
def options(path, *args, &block) route 'OPTIONS', path, *args, &block end
|
|
360
|
+
def patch(path, *args, &block) route 'PATCH', path, *args, &block end
|
|
361
|
+
def link(path, *args, &block) route 'LINK', path, *args, &block end
|
|
362
|
+
def unlink(path, *args, &block) route 'UNLINK', path, *args, &block end
|
|
363
|
+
|
|
364
|
+
def rebase_url(url)
|
|
365
|
+
if url.start_with?('/')
|
|
366
|
+
new_url = ''
|
|
367
|
+
new_url << conform_uri(ENV['RACK_BASE_URI']) if ENV['RACK_BASE_URI']
|
|
368
|
+
new_url << conform_uri(uri_root) if defined?(uri_root)
|
|
369
|
+
new_url << url
|
|
370
|
+
else
|
|
371
|
+
url.blank? ? '/' : url
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
##
|
|
376
|
+
# Processes the existing path and prepends the 'parent' parameters onto the route
|
|
377
|
+
# Used for calculating path in route method.
|
|
378
|
+
#
|
|
379
|
+
def process_path_for_parent_params(path, parent_params)
|
|
380
|
+
parent_prefix = parent_params.flatten.compact.uniq.map do |param|
|
|
381
|
+
map = (param.respond_to?(:map) && param.map ? param.map : param.to_s)
|
|
382
|
+
part = "#{map}/:#{param.to_s.singularize}_id/"
|
|
383
|
+
part = "(#{part})?" if param.respond_to?(:optional) && param.optional?
|
|
384
|
+
part
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
[parent_prefix, path].flatten.join("")
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
private
|
|
391
|
+
|
|
392
|
+
CONTROLLER_OPTIONS = [ :parent, :provides, :use_format, :cache, :expires, :map, :conditions, :accepts, :params ].freeze
|
|
393
|
+
|
|
394
|
+
# Saves controller options, yields the block, restores controller options.
|
|
395
|
+
def with_new_options(*args)
|
|
396
|
+
options = args.extract_options!
|
|
397
|
+
|
|
398
|
+
CONTROLLER_OPTIONS.each{ |key| replace_instance_variable("@_#{key}", options.delete(key)) }
|
|
399
|
+
replace_instance_variable(:@_controller, args)
|
|
400
|
+
replace_instance_variable(:@_defaults, options)
|
|
401
|
+
replace_instance_variable(:@filters, :before => @filters[:before].dup, :after => @filters[:after].dup)
|
|
402
|
+
replace_instance_variable(:@layout, nil)
|
|
403
|
+
|
|
404
|
+
yield
|
|
405
|
+
|
|
406
|
+
@original_instance.each do |key, value|
|
|
407
|
+
instance_variable_set(key, value)
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Sets instance variable by name and saves the original value in @original_instance hash
|
|
412
|
+
def replace_instance_variable(name, value)
|
|
413
|
+
@original_instance ||= {}
|
|
414
|
+
@original_instance[name] = instance_variable_get(name)
|
|
415
|
+
instance_variable_set(name, value)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Searches compiled router for a path responding to args and makes a path with params.
|
|
419
|
+
def make_path_with_params(args, params)
|
|
420
|
+
names, params_array = args.partition{ |arg| arg.is_a?(Symbol) }
|
|
421
|
+
name = names[0, 2].join(" ").to_sym
|
|
422
|
+
compiled_router.path(name, *(params_array << params))
|
|
423
|
+
rescue PathRouter::InvalidRouteException
|
|
424
|
+
raise Tennpipes::Routing::UnrecognizedException, "Route mapping for url(#{name.inspect}) could not be found"
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Parse params from the url method
|
|
428
|
+
def value_to_param(object)
|
|
429
|
+
case object
|
|
430
|
+
when Array
|
|
431
|
+
object.map { |item| value_to_param(item) }.compact
|
|
432
|
+
when Hash
|
|
433
|
+
object.inject({}) do |all, (key, value)|
|
|
434
|
+
next all if value.nil?
|
|
435
|
+
all[key] = value_to_param(value)
|
|
436
|
+
all
|
|
437
|
+
end
|
|
438
|
+
when nil
|
|
439
|
+
else
|
|
440
|
+
object.respond_to?(:to_param) ? object.to_param : object
|
|
441
|
+
end
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Add prefix slash if its not present and remove trailing slashes.
|
|
445
|
+
def conform_uri(uri_string)
|
|
446
|
+
uri_string.gsub(/^(?!\/)(.*)/, '/\1').gsub(/[\/]+$/, '')
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
##
|
|
450
|
+
# Rewrite default routes.
|
|
451
|
+
#
|
|
452
|
+
# @example
|
|
453
|
+
# get :index # => "/"
|
|
454
|
+
# get :index, "/" # => "/"
|
|
455
|
+
# get :index, :map => "/" # => "/"
|
|
456
|
+
# get :show, "/show-me" # => "/show-me"
|
|
457
|
+
# get :show, :map => "/show-me" # => "/show-me"
|
|
458
|
+
# get "/foo/bar" # => "/show"
|
|
459
|
+
# get :index, :parent => :user # => "/user/:user_id/index"
|
|
460
|
+
# get :show, :with => :id, :parent => :user # => "/user/:user_id/show/:id"
|
|
461
|
+
# get :show, :with => :id # => "/show/:id"
|
|
462
|
+
# get [:show, :id] # => "/show/:id"
|
|
463
|
+
# get :show, :with => [:id, :name] # => "/show/:id/:name"
|
|
464
|
+
# get [:show, :id, :name] # => "/show/:id/:name"
|
|
465
|
+
# get :list, :provides => :js # => "/list.{:format,js)"
|
|
466
|
+
# get :list, :provides => :any # => "/list(.:format)"
|
|
467
|
+
# get :list, :provides => [:js, :json] # => "/list.{!format,js|json}"
|
|
468
|
+
# get :list, :provides => [:html, :js, :json] # => "/list(.{!format,js|json})"
|
|
469
|
+
# get :list, :priority => :low # Defers route to be last
|
|
470
|
+
# get /pattern/, :name => :foo, :generate_with => '/foo' # Generates :foo as /foo
|
|
471
|
+
def route(verb, path, *args, &block)
|
|
472
|
+
options = case args.size
|
|
473
|
+
when 2
|
|
474
|
+
args.last.merge(:map => args.first)
|
|
475
|
+
when 1
|
|
476
|
+
map = args.shift if args.first.is_a?(String)
|
|
477
|
+
if args.first.is_a?(Hash)
|
|
478
|
+
map ? args.first.merge(:map => map) : args.first
|
|
479
|
+
else
|
|
480
|
+
{:map => map || args.first}
|
|
481
|
+
end
|
|
482
|
+
when 0
|
|
483
|
+
{}
|
|
484
|
+
else raise
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
route_options = options.dup
|
|
488
|
+
route_options[:provides] = @_provides if @_provides
|
|
489
|
+
route_options[:accepts] = @_accepts if @_accepts
|
|
490
|
+
route_options[:params] = @_params unless @_params.nil? || route_options.include?(:params)
|
|
491
|
+
|
|
492
|
+
# Add Sinatra condition to check rack-protection failure.
|
|
493
|
+
if protect_from_csrf && (report_csrf_failure || allow_disabled_csrf)
|
|
494
|
+
unless route_options.has_key?(:csrf_protection)
|
|
495
|
+
route_options[:csrf_protection] = true
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
path, *route_options[:with] = path if path.is_a?(Array)
|
|
500
|
+
action = path
|
|
501
|
+
path, name, route_parents, options, route_options = *parse_route(path, route_options, verb)
|
|
502
|
+
options.reverse_merge!(@_conditions) if @_conditions
|
|
503
|
+
|
|
504
|
+
method_name = "#{verb} #{path}"
|
|
505
|
+
unbound_method = generate_method(method_name, &block)
|
|
506
|
+
|
|
507
|
+
block_arity = block.arity
|
|
508
|
+
block = if block_arity == 0
|
|
509
|
+
proc{ |request, _| unbound_method.bind(request).call }
|
|
510
|
+
else
|
|
511
|
+
proc{ |request, block_params| unbound_method.bind(request).call(*block_params) }
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
invoke_hook(:route_added, verb, path, block)
|
|
515
|
+
|
|
516
|
+
path[0, 0] = "/" if path == "(.:format)?"
|
|
517
|
+
route = router.add(verb, path, route_options)
|
|
518
|
+
route.name = name if name
|
|
519
|
+
route.action = action
|
|
520
|
+
priority_name = options.delete(:priority) || :normal
|
|
521
|
+
priority = ROUTE_PRIORITY[priority_name] or raise("Priority #{priority_name} not recognized, try #{ROUTE_PRIORITY.keys.join(', ')}")
|
|
522
|
+
route.cache = options.key?(:cache) ? options.delete(:cache) : @_cache
|
|
523
|
+
route.cache_expires = options.key?(:expires) ? options.delete(:expires) : @_expires
|
|
524
|
+
route.parent = route_parents ? (route_parents.count == 1 ? route_parents.first : route_parents) : route_parents
|
|
525
|
+
route.host = options.delete(:host) if options.key?(:host)
|
|
526
|
+
route.user_agent = options.delete(:agent) if options.key?(:agent)
|
|
527
|
+
if options.key?(:default_values)
|
|
528
|
+
defaults = options.delete(:default_values)
|
|
529
|
+
#route.options[:default_values] = defaults if defaults
|
|
530
|
+
route.default_values = defaults if defaults
|
|
531
|
+
end
|
|
532
|
+
options.delete_if do |option, captures|
|
|
533
|
+
if route.significant_variable_names.include?(option)
|
|
534
|
+
route.capture[option] = Array(captures).first
|
|
535
|
+
true
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Add Sinatra conditions.
|
|
540
|
+
options.each do |option, _args|
|
|
541
|
+
option = :provides_format if option == :provides
|
|
542
|
+
route.respond_to?(option) ? route.send(option, *_args) : send(option, *_args)
|
|
543
|
+
end
|
|
544
|
+
conditions, @conditions = @conditions, []
|
|
545
|
+
route.custom_conditions.concat(conditions)
|
|
546
|
+
|
|
547
|
+
invoke_hook(:tennpipes_route_added, route, verb, path, args, options, block)
|
|
548
|
+
|
|
549
|
+
block_parameter_length = route.block_parameter_length
|
|
550
|
+
if block_arity > 0 && block_parameter_length != block_arity
|
|
551
|
+
fail BlockArityError.new(route.path, block_arity, block_parameter_length)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
# Add Application defaults.
|
|
555
|
+
route.before_filters << @filters[:before]
|
|
556
|
+
route.after_filters << @filters[:after]
|
|
557
|
+
if @_controller
|
|
558
|
+
route.use_layout = @layout
|
|
559
|
+
route.controller = Array(@_controller).join('/')
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
deferred_routes[priority] << [route, block]
|
|
563
|
+
|
|
564
|
+
route
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
##
|
|
568
|
+
# Returns the final parsed route details (modified to reflect all
|
|
569
|
+
# Tennpipes options) given the raw route. Raw route passed in could be
|
|
570
|
+
# a named alias or a string and is parsed to reflect provides formats,
|
|
571
|
+
# controllers, parents, 'with' parameters, and other options.
|
|
572
|
+
#
|
|
573
|
+
def parse_route(path, options, verb)
|
|
574
|
+
route_options = {}
|
|
575
|
+
|
|
576
|
+
if options[:params] == true
|
|
577
|
+
options.delete(:params)
|
|
578
|
+
elsif options.include?(:params)
|
|
579
|
+
options[:params] ||= []
|
|
580
|
+
options[:params] |= Array(options[:with]) if options[:with]
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# We need check if path is a symbol, if that it's a named route.
|
|
584
|
+
map = options.delete(:map)
|
|
585
|
+
|
|
586
|
+
# path i.e :index or :show
|
|
587
|
+
if path.kind_of?(Symbol)
|
|
588
|
+
name = path
|
|
589
|
+
path = map ? map.dup : (path == :index ? '/' : path.to_s)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
# Build our controller
|
|
593
|
+
controller = Array(@_controller).map(&:to_s)
|
|
594
|
+
|
|
595
|
+
case path
|
|
596
|
+
when String # path i.e "/index" or "/show"
|
|
597
|
+
# Now we need to parse our 'with' params
|
|
598
|
+
if with_params = options.delete(:with)
|
|
599
|
+
path = process_path_for_with_params(path, with_params)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
# Now we need to parse our provides
|
|
603
|
+
options.delete(:provides) if options[:provides].nil?
|
|
604
|
+
|
|
605
|
+
options.delete(:accepts) if options[:accepts].nil?
|
|
606
|
+
|
|
607
|
+
if @_use_format || options[:provides]
|
|
608
|
+
process_path_for_provides(path)
|
|
609
|
+
# options[:add_match_with] ||= {}
|
|
610
|
+
# options[:add_match_with][:format] = /[^\.]+/
|
|
611
|
+
end
|
|
612
|
+
|
|
613
|
+
absolute_map = map && map[0] == ?/
|
|
614
|
+
|
|
615
|
+
unless controller.empty?
|
|
616
|
+
# Now we need to add our controller path only if not mapped directly
|
|
617
|
+
if map.blank? and !absolute_map
|
|
618
|
+
controller_path = controller.join("/")
|
|
619
|
+
path.gsub!(%r{^\(/\)|/\?}, "")
|
|
620
|
+
path = File.join(controller_path, path) unless @_map
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
# Now we need to parse our 'parent' params and parent scope.
|
|
625
|
+
if !absolute_map and parent_params = options.delete(:parent) || @_parent
|
|
626
|
+
parent_params = (Array(@_parent) + Array(parent_params)).uniq
|
|
627
|
+
path = process_path_for_parent_params(path, parent_params)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
# Add any controller level map to the front of the path.
|
|
631
|
+
path = "#{@_map}/#{path}".squeeze('/') unless absolute_map or @_map.blank?
|
|
632
|
+
|
|
633
|
+
# Small reformats
|
|
634
|
+
path.gsub!(%r{/\?$}, '(/)') # Remove index path
|
|
635
|
+
path.gsub!(%r{//$}, '/') # Remove index path
|
|
636
|
+
path[0,0] = "/" if path !~ %r{^\(?/} # Paths must start with a /
|
|
637
|
+
path.sub!(%r{/(\))?$}, '\\1') if path != "/" # Remove latest trailing delimiter
|
|
638
|
+
path.gsub!(/\/(\(\.|$)/, '\\1') # Remove trailing slashes
|
|
639
|
+
path.squeeze!('/')
|
|
640
|
+
when Regexp
|
|
641
|
+
route_options[:path_for_generation] = options.delete(:generate_with) if options.key?(:generate_with)
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
name = options.delete(:route_name) if name.nil? && options.key?(:route_name)
|
|
645
|
+
name = options.delete(:name) if name.nil? && options.key?(:name)
|
|
646
|
+
if name
|
|
647
|
+
controller_name = controller.join("_")
|
|
648
|
+
name = "#{controller_name} #{name}".to_sym unless controller_name.blank?
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Merge in option defaults.
|
|
652
|
+
options.reverse_merge!(:default_values => @_defaults)
|
|
653
|
+
|
|
654
|
+
[path, name, parent_params, options, route_options]
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
##
|
|
658
|
+
# Processes the existing path and appends the 'with' parameters onto the route
|
|
659
|
+
# Used for calculating path in route method.
|
|
660
|
+
#
|
|
661
|
+
def process_path_for_with_params(path, with_params)
|
|
662
|
+
File.join(path, Array(with_params).map do |step|
|
|
663
|
+
step.kind_of?(String) ? step : step.inspect
|
|
664
|
+
end.join("/"))
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
##
|
|
668
|
+
# Processes the existing path and appends the 'format' suffix onto the route.
|
|
669
|
+
# Used for calculating path in route method.
|
|
670
|
+
#
|
|
671
|
+
def process_path_for_provides(path)
|
|
672
|
+
path << "(.:format)?" unless path[-11, 11] == '(.:format)?'
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
##
|
|
676
|
+
# Allows routing by MIME-types specified in the URL or ACCEPT header.
|
|
677
|
+
#
|
|
678
|
+
# By default, if a non-provided mime-type is specified in a URL, the
|
|
679
|
+
# route will not match an thus return a 404.
|
|
680
|
+
#
|
|
681
|
+
# Setting the :treat_format_as_accept option to true allows treating
|
|
682
|
+
# missing mime types specified in the URL as if they were specified
|
|
683
|
+
# in the ACCEPT header and thus return 406.
|
|
684
|
+
#
|
|
685
|
+
# If no type is specified, the first in the provides-list will be
|
|
686
|
+
# returned.
|
|
687
|
+
#
|
|
688
|
+
# @example
|
|
689
|
+
# get "/a", :provides => [:html, :js]
|
|
690
|
+
# # => GET /a => :html
|
|
691
|
+
# # => GET /a.js => :js
|
|
692
|
+
# # => GET /a.xml => 404
|
|
693
|
+
#
|
|
694
|
+
# get "/b", :provides => [:html]
|
|
695
|
+
# # => GET /b; ACCEPT: html => html
|
|
696
|
+
# # => GET /b; ACCEPT: js => 406
|
|
697
|
+
#
|
|
698
|
+
# enable :treat_format_as_accept
|
|
699
|
+
# get "/c", :provides => [:html, :js]
|
|
700
|
+
# # => GET /c.xml => 406
|
|
701
|
+
#
|
|
702
|
+
def provides(*types)
|
|
703
|
+
@_use_format = true
|
|
704
|
+
provides_format(*types)
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
def provides_format(*types)
|
|
708
|
+
mime_types = types.map{ |type| mime_type(CONTENT_TYPE_ALIASES[type] || type) }
|
|
709
|
+
condition do
|
|
710
|
+
return provides_format?(types, params[:format].to_sym) if params[:format]
|
|
711
|
+
|
|
712
|
+
accepts = request.accept.map(&:to_str)
|
|
713
|
+
# Per rfc2616-sec14:
|
|
714
|
+
# Assume */* if no ACCEPT header is given.
|
|
715
|
+
catch_all = accepts.delete("*/*")
|
|
716
|
+
|
|
717
|
+
return provides_any?(accepts) if types.include?(:any)
|
|
718
|
+
|
|
719
|
+
accepts = accepts.empty? ? mime_types.slice(0,1) : (accepts & mime_types)
|
|
720
|
+
|
|
721
|
+
type = accepts.first && mime_symbol(accepts.first)
|
|
722
|
+
type ||= catch_all && types.first
|
|
723
|
+
|
|
724
|
+
accept_format = CONTENT_TYPE_ALIASES[type] || type
|
|
725
|
+
if types.include?(accept_format)
|
|
726
|
+
content_type(accept_format || :html, :charset => 'utf-8')
|
|
727
|
+
else
|
|
728
|
+
halt 406 unless catch_all
|
|
729
|
+
false
|
|
730
|
+
end
|
|
731
|
+
end
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
##
|
|
735
|
+
# Allows routing by Media type.
|
|
736
|
+
#
|
|
737
|
+
# @example
|
|
738
|
+
# get "/a", :accepts => [:html, :js]
|
|
739
|
+
# # => GET /a CONTENT_TYPE text/html => :html
|
|
740
|
+
# # => GET /a CONTENT_TYPE application/javascript => :js
|
|
741
|
+
# # => GET /a CONTENT_TYPE application/xml => 406
|
|
742
|
+
#
|
|
743
|
+
def accepts(*types)
|
|
744
|
+
mime_types = types.map{ |type| mime_type(CONTENT_TYPE_ALIASES[type] || type) }
|
|
745
|
+
condition do
|
|
746
|
+
halt 406 unless mime_types.include?(request.media_type)
|
|
747
|
+
content_type(mime_symbol(request.media_type), :charset => 'utf-8')
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
##
|
|
752
|
+
# Implements checking for rack-protection failure flag when
|
|
753
|
+
# `report_csrf_failure` is enabled.
|
|
754
|
+
#
|
|
755
|
+
# @example
|
|
756
|
+
# post("/", :csrf_protection => false)
|
|
757
|
+
#
|
|
758
|
+
def csrf_protection(enabled)
|
|
759
|
+
return unless enabled
|
|
760
|
+
condition do
|
|
761
|
+
if request.env['protection.csrf.failed']
|
|
762
|
+
message = settings.protect_from_csrf.kind_of?(Hash) && settings.protect_from_csrf[:message] || 'Forbidden'
|
|
763
|
+
halt(403, message)
|
|
764
|
+
end
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
##
|
|
770
|
+
# Instance methods related to recognizing and processing routes and serving static files.
|
|
771
|
+
#
|
|
772
|
+
module InstanceMethods
|
|
773
|
+
##
|
|
774
|
+
# Instance method for URL generation.
|
|
775
|
+
#
|
|
776
|
+
# @example
|
|
777
|
+
# url(:show, :id => 1)
|
|
778
|
+
# url(:show, :name => :test)
|
|
779
|
+
# url(:show, 1)
|
|
780
|
+
# url("/foo", false, false)
|
|
781
|
+
#
|
|
782
|
+
# @see Tennpipes::Routing::ClassMethods#url
|
|
783
|
+
#
|
|
784
|
+
def url(*args)
|
|
785
|
+
if args.first.is_a?(String)
|
|
786
|
+
url_path = settings.rebase_url(args.shift)
|
|
787
|
+
if args.empty?
|
|
788
|
+
url_path
|
|
789
|
+
else
|
|
790
|
+
# Delegate sinatra-style urls to Sinatra. Ex: url("/foo", false, false)
|
|
791
|
+
# http://www.sinatrarb.com/intro#Generating%20URLs
|
|
792
|
+
super url_path, *args
|
|
793
|
+
end
|
|
794
|
+
else
|
|
795
|
+
# Delegate to Tennpipes named route URL generation.
|
|
796
|
+
settings.url(*args)
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
alias :url_for :url
|
|
800
|
+
|
|
801
|
+
##
|
|
802
|
+
# Returns absolute url. Calls Sinatra::Helpers#uri to generate protocol version, hostname and port.
|
|
803
|
+
#
|
|
804
|
+
# @example
|
|
805
|
+
# absolute_url(:show, :id => 1) # => http://example.com/show?id=1
|
|
806
|
+
# absolute_url(:show, 24) # => https://example.com/admin/show/24
|
|
807
|
+
# absolute_url('/foo/bar') # => https://example.com/admin/foo/bar
|
|
808
|
+
# absolute_url('baz') # => https://example.com/admin/foo/baz
|
|
809
|
+
#
|
|
810
|
+
def absolute_url(*args)
|
|
811
|
+
url_path = args.shift
|
|
812
|
+
if url_path.is_a?(String) && !url_path.start_with?('/')
|
|
813
|
+
url_path = request.env['PATH_INFO'].rpartition('/').first << '/' << url_path
|
|
814
|
+
end
|
|
815
|
+
uri url(url_path, *args), true, false
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def recognize_path(path)
|
|
819
|
+
settings.recognize_path(path)
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
##
|
|
823
|
+
# Returns the current path within a route from specified +path_params+.
|
|
824
|
+
#
|
|
825
|
+
def current_path(*path_params)
|
|
826
|
+
if path_params.last.is_a?(Hash)
|
|
827
|
+
path_params[-1] = params.merge(path_params[-1].with_indifferent_access)
|
|
828
|
+
else
|
|
829
|
+
path_params << params
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
path_params[-1] = path_params[-1].symbolize_keys
|
|
833
|
+
@route.path(*path_params)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
##
|
|
837
|
+
# Returns the current route
|
|
838
|
+
#
|
|
839
|
+
# @example
|
|
840
|
+
# -if route.controller == :press
|
|
841
|
+
# %li=show_article
|
|
842
|
+
#
|
|
843
|
+
def route
|
|
844
|
+
@route
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
##
|
|
848
|
+
# This is mostly just a helper so request.path_info isn't changed when
|
|
849
|
+
# serving files from the public directory.
|
|
850
|
+
#
|
|
851
|
+
def static_file?(path_info)
|
|
852
|
+
return unless public_dir = settings.public_folder
|
|
853
|
+
public_dir = File.expand_path(public_dir)
|
|
854
|
+
path = File.expand_path(public_dir + unescape(path_info))
|
|
855
|
+
return unless path.start_with?(public_dir)
|
|
856
|
+
return unless File.file?(path)
|
|
857
|
+
return path
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
#
|
|
861
|
+
# Method for deliver static files.
|
|
862
|
+
#
|
|
863
|
+
def static!(options = {})
|
|
864
|
+
if path = static_file?(request.path_info)
|
|
865
|
+
env['sinatra.static_file'] = path
|
|
866
|
+
cache_control(*settings.static_cache_control) if settings.static_cache_control?
|
|
867
|
+
send_file(path, options)
|
|
868
|
+
end
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
##
|
|
872
|
+
# Return the request format, this is useful when we need to respond to
|
|
873
|
+
# a given Content-Type.
|
|
874
|
+
#
|
|
875
|
+
# @param [Symbol, nil] type
|
|
876
|
+
#
|
|
877
|
+
# @param [Hash] params
|
|
878
|
+
#
|
|
879
|
+
# @example
|
|
880
|
+
# get :index, :provides => :any do
|
|
881
|
+
# case content_type
|
|
882
|
+
# when :js then ...
|
|
883
|
+
# when :json then ...
|
|
884
|
+
# when :html then ...
|
|
885
|
+
# end
|
|
886
|
+
# end
|
|
887
|
+
#
|
|
888
|
+
def content_type(type=nil, params={})
|
|
889
|
+
return @_content_type unless type
|
|
890
|
+
params.delete(:charset) if type == :json
|
|
891
|
+
super(type, params)
|
|
892
|
+
@_content_type = type
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
private
|
|
896
|
+
|
|
897
|
+
def provides_any?(formats)
|
|
898
|
+
accepted_format = formats.first
|
|
899
|
+
type = accepted_format ? mime_symbol(accepted_format) : :html
|
|
900
|
+
content_type(CONTENT_TYPE_ALIASES[type] || type, :charset => 'utf-8')
|
|
901
|
+
end
|
|
902
|
+
|
|
903
|
+
def provides_format?(types, format)
|
|
904
|
+
if ([:any, format] & types).empty?
|
|
905
|
+
# Per rfc2616-sec14:
|
|
906
|
+
# Answer with 406 if accept is given but types to not match any provided type.
|
|
907
|
+
halt 406 if settings.respond_to?(:treat_format_as_accept) && settings.treat_format_as_accept
|
|
908
|
+
false
|
|
909
|
+
else
|
|
910
|
+
content_type(format || :html, :charset => 'utf-8')
|
|
911
|
+
end
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
def mime_symbol(media_type)
|
|
915
|
+
::Rack::Mime::MIME_TYPES.key(media_type).sub(/\./,'').to_sym
|
|
916
|
+
end
|
|
917
|
+
|
|
918
|
+
def filter!(type, base=settings)
|
|
919
|
+
base.filters[type].each { |block| instance_eval(&block) }
|
|
920
|
+
end
|
|
921
|
+
|
|
922
|
+
def dispatch!
|
|
923
|
+
invoke do
|
|
924
|
+
static! if settings.static? && (request.get? || request.head?)
|
|
925
|
+
route!
|
|
926
|
+
end
|
|
927
|
+
rescue ::Exception => boom
|
|
928
|
+
filter! :before if boom.kind_of? ::Sinatra::NotFound
|
|
929
|
+
invoke { @boom_handled = handle_exception!(boom) }
|
|
930
|
+
ensure
|
|
931
|
+
@boom_handled or begin
|
|
932
|
+
filter! :after unless env['sinatra.static_file']
|
|
933
|
+
rescue ::Exception => boom
|
|
934
|
+
invoke { handle_exception!(boom) } unless @env['sinatra.error']
|
|
935
|
+
end
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
def route!(base = settings, pass_block = nil)
|
|
939
|
+
Thread.current['tennpipes.instance'] = self
|
|
940
|
+
first_time = true
|
|
941
|
+
|
|
942
|
+
routes = base.compiled_router.call(@request) do |route, params|
|
|
943
|
+
next if route.user_agent && !(route.user_agent =~ @request.user_agent)
|
|
944
|
+
original_params, parent_layout = @params.dup, @layout
|
|
945
|
+
returned_pass_block = invoke_route(route, params, first_time)
|
|
946
|
+
pass_block = returned_pass_block if returned_pass_block
|
|
947
|
+
first_time = false if first_time
|
|
948
|
+
@params, @layout = original_params, parent_layout
|
|
949
|
+
end
|
|
950
|
+
|
|
951
|
+
if routes.present?
|
|
952
|
+
verb = request.request_method
|
|
953
|
+
candidacies, allows = routes.partition{|route| route.verb == verb }
|
|
954
|
+
if candidacies.empty?
|
|
955
|
+
response["Allows"] = allows.map(&:verb).join(", ")
|
|
956
|
+
halt 405
|
|
957
|
+
end
|
|
958
|
+
end
|
|
959
|
+
|
|
960
|
+
if base.superclass.respond_to?(:router)
|
|
961
|
+
route!(base.superclass, pass_block)
|
|
962
|
+
return
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
route_eval(&pass_block) if pass_block
|
|
966
|
+
route_missing
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
def invoke_route(route, params, first_time)
|
|
970
|
+
@_response_buffer = nil
|
|
971
|
+
@route = request.route_obj = route
|
|
972
|
+
captured_params = captures_from_params(params)
|
|
973
|
+
|
|
974
|
+
@params.merge!(params) if params.kind_of?(Hash)
|
|
975
|
+
@params.merge!(:captures => captured_params) if !captured_params.empty? && route.path.is_a?(Regexp)
|
|
976
|
+
|
|
977
|
+
filter! :before if first_time
|
|
978
|
+
|
|
979
|
+
catch(:pass) do
|
|
980
|
+
begin
|
|
981
|
+
(route.before_filters - settings.filters[:before]).each{|block| instance_eval(&block) }
|
|
982
|
+
@layout = route.use_layout if route.use_layout
|
|
983
|
+
route.custom_conditions.each {|block| pass if block.bind(self).call == false }
|
|
984
|
+
route_response = route.block[self, captured_params]
|
|
985
|
+
@_response_buffer = route_response.instance_of?(Array) ? route_response.last : route_response
|
|
986
|
+
halt(route_response)
|
|
987
|
+
ensure
|
|
988
|
+
(route.after_filters - settings.filters[:after]).each {|block| instance_eval(&block) }
|
|
989
|
+
end
|
|
990
|
+
end
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
def captures_from_params(params)
|
|
994
|
+
if params[:captures].instance_of?(Array) && params[:captures].present?
|
|
995
|
+
params.delete(:captures)
|
|
996
|
+
else
|
|
997
|
+
params.values_at(*route.matcher.names).flatten
|
|
998
|
+
end
|
|
999
|
+
end
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
end
|