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