padrino-core 0.18.2 → 0.18.4
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 +4 -4
- data/lib/padrino-core/application/authenticity_token.rb +18 -2
- data/lib/padrino-core/application/routing.rb +155 -12
- data/lib/padrino-core/filter.rb +177 -13
- data/lib/padrino-core/path_router/compiler.rb +9 -17
- data/lib/padrino-core/path_router/matcher.rb +8 -2
- data/lib/padrino-core/path_router/route.rb +2 -1
- data/lib/padrino-core/version.rb +1 -1
- data/test/test_csrf_protection.rb +255 -0
- data/test/test_filters.rb +1391 -0
- data/test/test_routing.rb +186 -0
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e40ac3a18ee55e92777a0cffe27c3356801c5ff773df28798d7208b7022d127
|
|
4
|
+
data.tar.gz: 586a5335ad2611ffe9600dbe85e09e4499f9698bc3c37456039234ef519512bf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 457e2ddeb208461c25e4b5f4b81b090140ce9399db212969ca8da0114d9290cf2d721740173b13240b22b52e8934e1494a9db4209ba01f785ea84bb119cf7885
|
|
7
|
+
data.tar.gz: 30c4a57736742138bc8b283f1450b2d78ceb4c208a0f3ff0a29be1518c66b26da6c51dfe729cd994b749387fd348fb57ecb310bade5f7c3544e0edd594272fcb
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
module Padrino
|
|
2
2
|
class AuthenticityToken < Rack::Protection::AuthenticityToken
|
|
3
|
+
# A path with more dots than this isn't exempted by a Regexp: each dot adds forms to compare.
|
|
4
|
+
MAX_DOTS = 16
|
|
5
|
+
|
|
3
6
|
def initialize(app, options = {})
|
|
4
7
|
@app = app
|
|
5
8
|
@except = options[:except]
|
|
@@ -19,8 +22,21 @@ module Padrino
|
|
|
19
22
|
return false unless @except
|
|
20
23
|
return @except.call(env) if @except.is_a?(Proc)
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
# A request is excepted only if every form of the path that can reach a route is. This
|
|
26
|
+
# middleware has no route to ask for a format, so the forms without one count too, unless
|
|
27
|
+
# String entries name the path as it is. A path with a newline is never excepted.
|
|
28
|
+
forms = Routing.path_forms(env['PATH_INFO'].to_s)
|
|
29
|
+
return false if forms.any? { |form| form.include?("\n") }
|
|
30
|
+
return true if forms.all? { |form| @except.include?(form) }
|
|
31
|
+
return false if forms.any? { |form| form.count('.') > MAX_DOTS }
|
|
32
|
+
|
|
33
|
+
Routing.served_forms(forms).all? { |form| excepted?(form) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def excepted?(form)
|
|
39
|
+
@except.any? { |path| path.is_a?(Regexp) ? path.match?(form) : path == form }
|
|
24
40
|
end
|
|
25
41
|
end
|
|
26
42
|
end
|
|
@@ -17,6 +17,8 @@ module Padrino
|
|
|
17
17
|
CONTENT_TYPE_ALIASES = { htm: :html } unless defined?(CONTENT_TYPE_ALIASES)
|
|
18
18
|
# Defines the available route priorities supporting route deferrals.
|
|
19
19
|
ROUTE_PRIORITY = { high: 0, normal: 1, low: 2 } unless defined?(ROUTE_PRIORITY)
|
|
20
|
+
# What without_extra_slashes gives for a path that doesn't end in a slash.
|
|
21
|
+
NO_EXTRA_SLASHES = [].freeze unless defined?(NO_EXTRA_SLASHES)
|
|
20
22
|
|
|
21
23
|
# Raised when a route was invalid or cannot be processed.
|
|
22
24
|
class UnrecognizedException < RuntimeError; end
|
|
@@ -51,6 +53,126 @@ module Padrino
|
|
|
51
53
|
app.extend(ClassMethods)
|
|
52
54
|
end
|
|
53
55
|
alias included registered
|
|
56
|
+
|
|
57
|
+
##
|
|
58
|
+
# Decodes the %XX escapes in a request path, %2F included; a '+' stays a
|
|
59
|
+
# plus, as it does for the router and Sinatra. Returns a new UTF-8 string
|
|
60
|
+
# and leaves +path+ untouched.
|
|
61
|
+
#
|
|
62
|
+
# Invalid UTF-8 (such as '/%ff') is kept byte for byte, so the result's
|
|
63
|
+
# +valid_encoding?+ is false: replacing it would name a different file.
|
|
64
|
+
# The router matches no route for such a path; callers that compare the
|
|
65
|
+
# result with Regexps scrub it first.
|
|
66
|
+
#
|
|
67
|
+
# @example
|
|
68
|
+
# Padrino::Routing.decode_path('/caf%C3%A9/a+b') # => "/café/a+b"
|
|
69
|
+
#
|
|
70
|
+
# @api private
|
|
71
|
+
def decode_path(path)
|
|
72
|
+
Rack::Utils.unescape_path(path.b).force_encoding(Encoding::UTF_8)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
##
|
|
76
|
+
# Decodes a request path into the form routes are matched against: like
|
|
77
|
+
# decode_path, but an encoded slash stays '%2F' (from '%2F' or '%2f'), so
|
|
78
|
+
# it can't split a path segment. Invalid UTF-8 and malformed escapes are
|
|
79
|
+
# kept as they are; the router matches no route for such a path.
|
|
80
|
+
#
|
|
81
|
+
# @example
|
|
82
|
+
# Padrino::Routing.decode_route_path('/%70osts%2F1') # => "/posts%2F1"
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
def decode_route_path(path)
|
|
86
|
+
path.b.split(/%2F/i, -1).map { |segment| decode_path(segment) }.join('%2F').force_encoding(Encoding::UTF_8)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
##
|
|
90
|
+
# The forms a request path is compared in, router form first: as the
|
|
91
|
+
# router matches it (decode_route_path), fully decoded (decode_path), and
|
|
92
|
+
# as route params receive it (the router form decoded again, as a splat
|
|
93
|
+
# or param value is). Invalid UTF-8 is scrubbed so Regexps can match it;
|
|
94
|
+
# duplicates are dropped. +route_path+ is decode_route_path(path), for a
|
|
95
|
+
# caller that has it already.
|
|
96
|
+
#
|
|
97
|
+
# @example
|
|
98
|
+
# Padrino::Routing.path_forms('/files%252Freport') # => ["/files%2Freport", "/files/report"]
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
def path_forms(path, route_path = decode_route_path(path))
|
|
102
|
+
return [route_path.scrub] unless path.include?('%')
|
|
103
|
+
|
|
104
|
+
[route_path, decode_path(path), decode_path(route_path)].map(&:scrub).uniq
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
##
|
|
108
|
+
# Whether a provides condition for +types+ accepts +format+, a Symbol
|
|
109
|
+
# decoded as route params receive it.
|
|
110
|
+
#
|
|
111
|
+
# @example
|
|
112
|
+
# Padrino::Routing.provided_format?(%i[html json], :json) # => true
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
def provided_format?(types, format)
|
|
116
|
+
!([:any, format] & types).empty?
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
##
|
|
120
|
+
# Whether an app with +settings+ answers a format its provides
|
|
121
|
+
# conditions don't accept with 406 itself (treat_format_as_accept),
|
|
122
|
+
# instead of handing the request on.
|
|
123
|
+
#
|
|
124
|
+
# @api private
|
|
125
|
+
def treat_format_as_accept?(settings)
|
|
126
|
+
settings.respond_to?(:treat_format_as_accept) && settings.treat_format_as_accept ? true : false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
##
|
|
130
|
+
# A path with trailing slashes also counts with none and with one, as the
|
|
131
|
+
# router serves '/posts/' from a '/posts' route. A path that doesn't end
|
|
132
|
+
# in a slash gives [], and a path of slashes only gives ['', '/'].
|
|
133
|
+
#
|
|
134
|
+
# @example
|
|
135
|
+
# Padrino::Routing.without_extra_slashes('/posts//') # => ["/posts", "/posts/"]
|
|
136
|
+
#
|
|
137
|
+
# @api private
|
|
138
|
+
def without_extra_slashes(path)
|
|
139
|
+
return NO_EXTRA_SLASHES unless path.end_with?('/')
|
|
140
|
+
|
|
141
|
+
index = path.rindex(%r{[^/]})
|
|
142
|
+
stripped = index ? path[0..index] : ''
|
|
143
|
+
[stripped, "#{stripped}/"]
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
##
|
|
147
|
+
# Yields the forms the router may serve a request path as, without
|
|
148
|
+
# knowing the route: each of +forms+ (path_forms) and, for each form
|
|
149
|
+
# found, the form without its trailing slashes and with one
|
|
150
|
+
# (without_extra_slashes), and the form up to the last dot of its last
|
|
151
|
+
# segment, as a route can capture what follows as a format. Walking
|
|
152
|
+
# those in turn gives the form up to each such dot. Each form is yielded
|
|
153
|
+
# once, as it is found, so a caller can stop early. Returns an
|
|
154
|
+
# Enumerator without a block.
|
|
155
|
+
#
|
|
156
|
+
# @example
|
|
157
|
+
# Padrino::Routing.served_forms(['/feed.json//']).to_a # => ["/feed.json//", "/feed.json", "/feed.json/", "/feed"]
|
|
158
|
+
#
|
|
159
|
+
# @api private
|
|
160
|
+
def served_forms(forms)
|
|
161
|
+
return enum_for(__method__, forms) unless block_given?
|
|
162
|
+
|
|
163
|
+
seen = {}
|
|
164
|
+
queue = forms.dup
|
|
165
|
+
while (form = queue.shift)
|
|
166
|
+
next if seen.key?(form)
|
|
167
|
+
|
|
168
|
+
seen[form] = true
|
|
169
|
+
yield form
|
|
170
|
+
queue.concat(without_extra_slashes(form).reject(&:empty?))
|
|
171
|
+
slash = form.rindex('/').to_i
|
|
172
|
+
last = form.rindex('.')
|
|
173
|
+
queue << form[0, last] if last && last > slash
|
|
174
|
+
end
|
|
175
|
+
end
|
|
54
176
|
end
|
|
55
177
|
|
|
56
178
|
# Class methods responsible for enhanced routing for controllers.
|
|
@@ -291,6 +413,8 @@ module Padrino
|
|
|
291
413
|
|
|
292
414
|
def reset_router!
|
|
293
415
|
@deferred_routes = nil
|
|
416
|
+
# Keep only the provides formats of conditions no route has taken yet (provides_format).
|
|
417
|
+
@_provided_formats&.keep_if { |cond, _| @conditions.any? { |pending| pending.equal?(cond) } }
|
|
294
418
|
router.reset!
|
|
295
419
|
end
|
|
296
420
|
|
|
@@ -569,6 +693,7 @@ module Padrino
|
|
|
569
693
|
|
|
570
694
|
conditions, @conditions = @conditions, []
|
|
571
695
|
route.custom_conditions.concat(conditions)
|
|
696
|
+
route.provided_formats = conditions.filter_map { |cond| @_provided_formats[cond] } if @_provided_formats
|
|
572
697
|
|
|
573
698
|
invoke_hook(:padrino_route_added, route, verb, path, args, options, block)
|
|
574
699
|
|
|
@@ -730,6 +855,7 @@ module Padrino
|
|
|
730
855
|
provides_format(*types)
|
|
731
856
|
end
|
|
732
857
|
|
|
858
|
+
# Each route records the formats of its provides conditions (PathRouter::Route#provided_formats).
|
|
733
859
|
def provides_format(*types)
|
|
734
860
|
mime_types = types.map { |type| mime_type(CONTENT_TYPE_ALIASES[type] || type) }
|
|
735
861
|
condition do
|
|
@@ -754,6 +880,8 @@ module Padrino
|
|
|
754
880
|
catch_all ? true : halt(406)
|
|
755
881
|
end
|
|
756
882
|
end
|
|
883
|
+
(@_provided_formats ||= {}.compare_by_identity)[@conditions.last] = types
|
|
884
|
+
@conditions
|
|
757
885
|
end
|
|
758
886
|
|
|
759
887
|
##
|
|
@@ -876,8 +1004,7 @@ module Padrino
|
|
|
876
1004
|
#
|
|
877
1005
|
def static_file?(path_info)
|
|
878
1006
|
return unless (public_dir = settings.public_folder)
|
|
879
|
-
|
|
880
|
-
file = Rack::Utils.unescape_path(path_info.b).force_encoding(Encoding::UTF_8)
|
|
1007
|
+
file = Routing.decode_path(path_info)
|
|
881
1008
|
[File.expand_path(public_dir), settings.shared_public_folder].compact.each do |dir|
|
|
882
1009
|
path = "#{dir}#{file}"
|
|
883
1010
|
next unless Rack::Utils.valid_path?(path)
|
|
@@ -930,14 +1057,12 @@ module Padrino
|
|
|
930
1057
|
end
|
|
931
1058
|
|
|
932
1059
|
def provides_format?(types, format)
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
content_type(format || :html)
|
|
940
|
-
end
|
|
1060
|
+
return content_type(format || :html) if Routing.provided_format?(types, format)
|
|
1061
|
+
|
|
1062
|
+
# Per rfc2616-sec14:
|
|
1063
|
+
# Answer with 406 if accept is given but types to not match any provided type.
|
|
1064
|
+
halt 406 if Routing.treat_format_as_accept?(settings)
|
|
1065
|
+
false
|
|
941
1066
|
end
|
|
942
1067
|
|
|
943
1068
|
def mime_symbol(media_type)
|
|
@@ -949,7 +1074,20 @@ module Padrino
|
|
|
949
1074
|
base.filters[type].each { |block| instance_eval(&block) }
|
|
950
1075
|
end
|
|
951
1076
|
|
|
1077
|
+
# Runs the app's before filters for the route being tried. Each runs to its end at most once
|
|
1078
|
+
# for a level of routes, so one with no match on a route, or that passed on it, gets another
|
|
1079
|
+
# chance on the next, and a request that ends up elsewhere doesn't run them again.
|
|
1080
|
+
def before_filter!(base = settings)
|
|
1081
|
+
before_filter!(base.superclass) if base.superclass.respond_to?(:filters)
|
|
1082
|
+
base.filters[:before].each do |block|
|
|
1083
|
+
next if @_ran_filters[block]
|
|
1084
|
+
|
|
1085
|
+
@_ran_filters[block] = true unless Filter::SKIPPED.equal?(instance_eval(&block))
|
|
1086
|
+
end
|
|
1087
|
+
end
|
|
1088
|
+
|
|
952
1089
|
def dispatch!
|
|
1090
|
+
@_ran_filters = {}.compare_by_identity
|
|
953
1091
|
@params = defined?(Sinatra::IndifferentHash) ? Sinatra::IndifferentHash[@request.params] : indifferent_params(@request.params)
|
|
954
1092
|
force_encoding(@params)
|
|
955
1093
|
invoke do
|
|
@@ -957,7 +1095,7 @@ module Padrino
|
|
|
957
1095
|
route!
|
|
958
1096
|
end
|
|
959
1097
|
rescue ::Exception => e
|
|
960
|
-
|
|
1098
|
+
catch(:pass) { before_filter! } if e.is_a? ::Sinatra::NotFound
|
|
961
1099
|
invoke { @boom_handled = handle_exception!(e) }
|
|
962
1100
|
ensure
|
|
963
1101
|
@boom_handled || begin
|
|
@@ -1013,7 +1151,12 @@ module Padrino
|
|
|
1013
1151
|
@params[:format] = params[:format] if params[:format]
|
|
1014
1152
|
@params[:captures] = captured_params if !captured_params.empty? && route.path.is_a?(Regexp)
|
|
1015
1153
|
|
|
1016
|
-
|
|
1154
|
+
@_ran_filters.clear if first_time
|
|
1155
|
+
passed = catch(:pass) do
|
|
1156
|
+
before_filter!
|
|
1157
|
+
:filtered
|
|
1158
|
+
end
|
|
1159
|
+
return passed unless passed == :filtered
|
|
1017
1160
|
|
|
1018
1161
|
catch(:pass) do
|
|
1019
1162
|
(route.before_filters - settings.filters[:before]).each { |block| instance_eval(&block) }
|
data/lib/padrino-core/filter.rb
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
module Padrino
|
|
2
2
|
class Filter
|
|
3
|
+
PATH_OPTIONS = %i[path path_info].freeze
|
|
4
|
+
# The env keys that hold the forms of the request's path_info and path (path_entry), and route_cuts.
|
|
5
|
+
PATH_FORMS_KEYS = { path_info: 'padrino.decoded_path_info'.freeze, path: 'padrino.decoded_path'.freeze }.freeze
|
|
6
|
+
ROUTE_CUTS_KEY = 'padrino.route_cuts'.freeze
|
|
7
|
+
NO_ROUTE_CUTS = [[].freeze, [].freeze].freeze
|
|
8
|
+
NOT_FORMAT_DELIMITER = %r{[^./]}.freeze
|
|
9
|
+
MAX_FORMAT_DELIMITER = 3 # ceiling: the longest delimiter format_cuts takes before a format; raise it for a route with a longer one
|
|
10
|
+
# What a filter's proc returns when it doesn't apply to the route, so it is decided again.
|
|
11
|
+
SKIPPED = Object.new.freeze
|
|
12
|
+
|
|
3
13
|
attr_reader :block
|
|
4
14
|
|
|
5
15
|
def initialize(mode, scoped_controller, options, args, &block)
|
|
@@ -10,18 +20,18 @@ module Padrino
|
|
|
10
20
|
@block = block
|
|
11
21
|
end
|
|
12
22
|
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
# +settings+, the settings of the app running the request, decide how its routes answer a
|
|
24
|
+
# format they don't provide (format_cuts).
|
|
25
|
+
def apply?(request, settings = nil)
|
|
26
|
+
detect = match_with_arguments?(request, settings) || match_with_options?(request, settings)
|
|
15
27
|
detect ^ !@mode
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
def to_proc
|
|
19
|
-
if @args.empty? && @options.empty?
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
proc { instance_eval(&filter.block) if filter.apply?(request) }
|
|
24
|
-
end
|
|
31
|
+
return block if @args.empty? && @options.empty?
|
|
32
|
+
|
|
33
|
+
filter = self
|
|
34
|
+
proc { filter.apply?(request, settings) ? instance_eval(&filter.block) : SKIPPED }
|
|
25
35
|
end
|
|
26
36
|
|
|
27
37
|
private
|
|
@@ -30,23 +40,177 @@ module Padrino
|
|
|
30
40
|
@scoped_controller_name ||= Array(@scoped_controller).join('_')
|
|
31
41
|
end
|
|
32
42
|
|
|
33
|
-
def match_with_arguments?(request)
|
|
43
|
+
def match_with_arguments?(request, settings)
|
|
34
44
|
route = request.route_obj
|
|
35
|
-
path = request.path_info
|
|
36
45
|
@args.any? do |argument|
|
|
37
46
|
if argument.instance_of?(Symbol)
|
|
38
47
|
next unless route
|
|
39
48
|
name = route.name
|
|
40
49
|
argument == name || name == [scoped_controller_name, argument].join(' ').to_sym
|
|
41
50
|
else
|
|
42
|
-
|
|
51
|
+
entry = path_entry(request)
|
|
52
|
+
match_path?(argument, request, settings, all_paths(entry), entry)
|
|
43
53
|
end
|
|
44
54
|
end
|
|
45
55
|
end
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
# +paths+ holds every decoded form of the path (all_paths), and for the path and path_info
|
|
58
|
+
# options the path as given too. A filter runs if one of them, or one of the forms route_forms
|
|
59
|
+
# gives for it, matches. An :except filter is skipped only if every one of them matches
|
|
60
|
+
# exactly, and none has a newline. +info+ is the path_entry of path_info, when the caller has it.
|
|
61
|
+
def match_path?(argument, request, settings, paths, info = nil)
|
|
62
|
+
return paths.all? { |path| !path.include?("\n") && argument === path } unless @mode
|
|
63
|
+
|
|
64
|
+
filter_paths(request, settings, paths, info || path_entry(request)).any? { |form| argument === form }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# +paths+ and the forms route_forms gives for each, once per request for each route run and
|
|
68
|
+
# each list of paths (with no route, kept on +entry+).
|
|
69
|
+
def filter_paths(request, settings, paths, entry)
|
|
70
|
+
suffixes, cuts, cache = route_cuts(request, settings, entry)
|
|
71
|
+
cache ||= (entry[:filter_paths] ||= {})
|
|
72
|
+
cache[paths] ||= paths.flat_map { |path| route_forms(path, suffixes, cuts) }.uniq.freeze
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# The router also serves '/posts/' from a '/posts' route (PathRouter::Compiler#match?), and
|
|
76
|
+
# '/posts.json' from a route that captures a format: +path+ also counts without each format
|
|
77
|
+
# suffix of the route, each of those without its trailing slashes and with one, and with each
|
|
78
|
+
# cut applied (a suffix of the path and what replaces it) when it ends with that suffix.
|
|
79
|
+
def route_forms(path, suffixes, cuts)
|
|
80
|
+
forms = [path]
|
|
81
|
+
suffixes.each { |suffix| forms << path.delete_suffix(suffix) }
|
|
82
|
+
forms.concat(forms.flat_map { |form| Routing.without_extra_slashes(form) })
|
|
83
|
+
cuts.each { |suffix, rest| forms << "#{path.delete_suffix(suffix)}#{rest}" if path.end_with?(suffix) }
|
|
84
|
+
forms
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# The format suffixes (format_suffixes) and cuts (format_cuts) of the route being run, and a
|
|
88
|
+
# cache for filter_paths, once per route, path and Routing.treat_format_as_accept? setting for
|
|
89
|
+
# the request. No suffixes or cuts, and no cache, without a route. Positive filters use both, as
|
|
90
|
+
# each gives forms the other doesn't: a suffix drops the format from every decoded form for any
|
|
91
|
+
# route that captures one, and a cut, for a route whose only capture is its format, also drops
|
|
92
|
+
# the delimiter before it ('/data' for '/data-json' from '/data(-:format)?').
|
|
93
|
+
def route_cuts(request, settings, entry)
|
|
94
|
+
route = request.route_obj
|
|
95
|
+
return NO_ROUTE_CUTS unless route
|
|
96
|
+
|
|
97
|
+
accept = Routing.treat_format_as_accept?(settings)
|
|
98
|
+
store = ((request.env[ROUTE_CUTS_KEY] ||= {})[accept] ||= {}.compare_by_identity)
|
|
99
|
+
memo(store, route, entry[:path]) do
|
|
100
|
+
[format_suffixes(route, all_paths(entry)), format_cuts(route, route_path(entry), accept), {}]
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# What each of +forms+ has after what +route+ takes as the format-less path.
|
|
105
|
+
def format_suffixes(route, forms)
|
|
106
|
+
forms.filter_map do |form|
|
|
107
|
+
match = route.match(form)
|
|
108
|
+
format_suffix(form, match) if match.respond_to?(:names) && match.names.include?('format')
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# A format ends the path, and a slash before it starts another segment, not a format. A route
|
|
113
|
+
# with an optional format can leave its dot behind without one.
|
|
114
|
+
def format_suffix(form, match)
|
|
115
|
+
offset = match.begin('format')
|
|
116
|
+
return suffix_after(form, form) unless offset
|
|
117
|
+
|
|
118
|
+
before = form[0...offset]
|
|
119
|
+
return if before.end_with?('/') || form.index(%r{[^/]}, match.end('format'))
|
|
120
|
+
|
|
121
|
+
suffix_after(form, before)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# What +form+ has after +before+ without its trailing delimiters, keeping one slash in the path.
|
|
125
|
+
def suffix_after(form, before)
|
|
126
|
+
size = (before.rindex(NOT_FORMAT_DELIMITER) || -1) + 1
|
|
127
|
+
form[size] == '/' ? form[(size + 1)..] : form[size..]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# For a route whose only capture is a format that it captured at the end of +form+, before
|
|
131
|
+
# slashes only, and that it serves (provided?): +form+ cut at the longest prefix, from the start
|
|
132
|
+
# of the format back to MAX_FORMAT_DELIMITER characters before it, that the same route matches
|
|
133
|
+
# without a format, and that prefix with one slash if the route matches that too. None for a
|
|
134
|
+
# route that requires its format, a route with another param or splat, and a Regexp route.
|
|
135
|
+
def format_cuts(route, form, accept)
|
|
136
|
+
matcher = route.matcher
|
|
137
|
+
return [] unless matcher.mustermann? && matcher.names == ['format']
|
|
138
|
+
|
|
139
|
+
match = route.match(form)
|
|
140
|
+
from, to = match.offset('format') if match
|
|
141
|
+
return [] unless from && from < to && form[to..].count('^/').zero? && provided?(route, match['format'], accept)
|
|
142
|
+
|
|
143
|
+
at = from.downto([from - MAX_FORMAT_DELIMITER, 0].max).find { |cut| without_format?(route, form[0, cut]) }
|
|
144
|
+
return [] unless at
|
|
145
|
+
|
|
146
|
+
suffix = form[at..]
|
|
147
|
+
without_format?(route, "#{form[0, at]}/") ? [[suffix, ''], [suffix, '/']] : [[suffix, '']]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Whether the route matches +path+ as the router does, also without one trailing slash, with no format.
|
|
151
|
+
def without_format?(route, path)
|
|
152
|
+
match = route.match(path)
|
|
153
|
+
match && match['format'].nil?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Whether the route serves +format+, as captured from the path the router matches: each of its
|
|
157
|
+
# provides conditions accepts it as route params receive it (provides_format?), or the app
|
|
158
|
+
# answers 406 for it itself instead of handing the request on (+accept+,
|
|
159
|
+
# Routing.treat_format_as_accept?).
|
|
160
|
+
def provided?(route, format, accept)
|
|
161
|
+
return true if accept
|
|
162
|
+
|
|
163
|
+
format = Routing.decode_path(format).scrub.to_sym
|
|
164
|
+
Array(route.provided_formats).all? { |types| Routing.provided_format?(types, format) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# The memo of the request's path_info or path (+name+) that the forms below are kept in, once
|
|
168
|
+
# per request, and again if the path changes (a mounted app sees its own).
|
|
169
|
+
def path_entry(request, name = :path_info)
|
|
170
|
+
path = request.send(name)
|
|
171
|
+
memo(request.env, PATH_FORMS_KEYS.fetch(name), path) { |stored| { path: stored } }
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# The path as the router matches it.
|
|
175
|
+
def route_path(entry)
|
|
176
|
+
entry[:route] ||= decoded_route_path(entry).scrub
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Every decoded form of the path, router form first (Routing.path_forms).
|
|
180
|
+
def all_paths(entry)
|
|
181
|
+
entry[:all] ||= Routing.path_forms(entry[:path], decoded_route_path(entry))
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# The path as given, scrubbed.
|
|
185
|
+
def raw_path(entry)
|
|
186
|
+
entry[:raw] ||= entry[:path].dup.force_encoding(Encoding::UTF_8).scrub
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def decoded_route_path(entry)
|
|
190
|
+
entry[:decoded] ||= Routing.decode_route_path(entry[:path])
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# The block's value for +key+ in +store+, computed once for each +path+. The block gets the
|
|
194
|
+
# copy of +path+ the memo keeps.
|
|
195
|
+
def memo(store, key, path)
|
|
196
|
+
cached = store[key]
|
|
197
|
+
return cached[1] if cached && cached[0] == path
|
|
198
|
+
|
|
199
|
+
stored = path.dup
|
|
200
|
+
value = yield(stored)
|
|
201
|
+
store[key] = [stored, value]
|
|
202
|
+
value
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def match_with_options?(request, settings)
|
|
48
206
|
user_agent = request.user_agent
|
|
49
|
-
@options.any?
|
|
207
|
+
@options.any? do |name, value|
|
|
208
|
+
next value === user_agent if name == :agent
|
|
209
|
+
next value === request.send(name) unless PATH_OPTIONS.include?(name)
|
|
210
|
+
|
|
211
|
+
entry = path_entry(request, name)
|
|
212
|
+
match_path?(value, request, settings, [raw_path(entry), *all_paths(entry)].uniq, name == :path_info ? entry : nil)
|
|
213
|
+
end
|
|
50
214
|
end
|
|
51
215
|
end
|
|
52
216
|
end
|
|
@@ -20,7 +20,7 @@ module Padrino
|
|
|
20
20
|
return if compiled?
|
|
21
21
|
@routes.each_with_index do |route, index|
|
|
22
22
|
route.index = index
|
|
23
|
-
route.regexp =
|
|
23
|
+
route.regexp = /\A(?<_#{index}>#{route.matcher.to_regexp})\z/
|
|
24
24
|
end
|
|
25
25
|
@compiled = true
|
|
26
26
|
end
|
|
@@ -93,26 +93,18 @@ module Padrino
|
|
|
93
93
|
# Decode env["PATH_INFO"]. Returns nil when the path doesn't decode to valid text
|
|
94
94
|
# (a malformed or non-UTF-8 escape such as "/%ff"), so it matches no route.
|
|
95
95
|
#
|
|
96
|
+
# A path that decodes to a newline, as routes see it or as route params decode it again
|
|
97
|
+
# (a '%0A' left by '%250A'), matches no route either.
|
|
98
|
+
#
|
|
96
99
|
def decode_pattern(pattern)
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
pattern = pattern.encode(Encoding.default_external)
|
|
101
|
+
return if pattern.match?(/%(?!\h\h)/)
|
|
102
|
+
|
|
103
|
+
decoded = Padrino::Routing.decode_route_path(pattern)
|
|
104
|
+
decoded if decoded.valid_encoding? && !decoded.match?(/\n|%0a/i)
|
|
99
105
|
rescue ArgumentError, EncodingError
|
|
100
106
|
nil
|
|
101
107
|
end
|
|
102
|
-
|
|
103
|
-
##
|
|
104
|
-
# Encode string with Encoding.default_external
|
|
105
|
-
#
|
|
106
|
-
def encode_default_external(string)
|
|
107
|
-
string.encode(Encoding.default_external)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
##
|
|
111
|
-
# Decode uri escape sequences
|
|
112
|
-
#
|
|
113
|
-
def decode_uri(string)
|
|
114
|
-
string.split(/%2F|%2f/, -1).map { |part| Rack::Utils.unescape(part.gsub('+', '%2B')) }.join('%2F')
|
|
115
|
-
end
|
|
116
108
|
end
|
|
117
109
|
end
|
|
118
110
|
end
|
|
@@ -19,13 +19,19 @@ module Padrino
|
|
|
19
19
|
# Matches a pattern with the route matcher.
|
|
20
20
|
#
|
|
21
21
|
def match(pattern)
|
|
22
|
-
if (match_data =
|
|
22
|
+
if (match_data = whole_regexp.match(pattern))
|
|
23
23
|
match_data
|
|
24
24
|
elsif pattern != '/' && pattern.end_with?('/')
|
|
25
|
-
|
|
25
|
+
whole_regexp.match(pattern[0..-2])
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# The handler, anchored to the whole path as PathRouter::Compiler matches routes
|
|
30
|
+
# (Mustermann's \Z and a Regexp route's ^$ stop before a newline).
|
|
31
|
+
def whole_regexp
|
|
32
|
+
@whole_regexp ||= to_regexp.is_a?(Regexp) ? /\A(?:#{to_regexp})\z/ : handler
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
##
|
|
30
36
|
# Returns a regexp from handler.
|
|
31
37
|
#
|
|
@@ -20,7 +20,8 @@ module Padrino
|
|
|
20
20
|
# The accessors will be used in other classes
|
|
21
21
|
#
|
|
22
22
|
attr_accessor :action, :cache, :cache_key, :cache_expires,
|
|
23
|
-
:parent, :use_layout, :controller, :user_agent, :path_for_generation, :default_values
|
|
23
|
+
:parent, :use_layout, :controller, :user_agent, :path_for_generation, :default_values,
|
|
24
|
+
:provided_formats
|
|
24
25
|
|
|
25
26
|
##
|
|
26
27
|
# Constructs an instance of PathRouter::Route.
|
data/lib/padrino-core/version.rb
CHANGED