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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf357f68006699d9e80fa024da18fbf0b64bd7d0365ea2ecfd90c12b0b8a6cfa
4
- data.tar.gz: f702562744c51a4dee46da71739b7864ba3ebc1c9c937ddb8d5af3a07efc391c
3
+ metadata.gz: 8e40ac3a18ee55e92777a0cffe27c3356801c5ff773df28798d7208b7022d127
4
+ data.tar.gz: 586a5335ad2611ffe9600dbe85e09e4499f9698bc3c37456039234ef519512bf
5
5
  SHA512:
6
- metadata.gz: 140423832026779eb7731e1145ab1d6d7b7197bf77e85d83c40077f639bcbc3fc0f7ee4ed2bedaee35a6b9b1188ae8ce9437e09fb605c17c605abfbf413c7383
7
- data.tar.gz: ea73969e005298405dd22072d18a211634d038a68e53b7c3b9cfead7d29b2471eb06f30b723702f50fa00ab331cdb1a5bc29cbdc98449be874629e91dab50e05
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
- path_info = env['PATH_INFO']
23
- @except.any? { |path| path.is_a?(Regexp) ? path.match(path_info) : path == path_info }
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
- # Decode the path the way Sinatra's static! and the router do: %XX only, so '+' stays a plus.
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
- if ([:any, format] & types).empty?
934
- # Per rfc2616-sec14:
935
- # Answer with 406 if accept is given but types to not match any provided type.
936
- halt 406 if settings.respond_to?(:treat_format_as_accept) && settings.treat_format_as_accept
937
- false
938
- else
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
- filter! :before if e.is_a? ::Sinatra::NotFound
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
- filter! :before if first_time
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) }
@@ -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
- def apply?(request)
14
- detect = match_with_arguments?(request) || match_with_options?(request)
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
- block
21
- else
22
- filter = self
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
- argument === path
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
- def match_with_options?(request)
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? { |name, value| value === (name == :agent ? user_agent : request.send(name)) }
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 = /(?<_#{index}>#{route.matcher.to_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
- decoded = decode_uri(encode_default_external(pattern))
98
- decoded if decoded.valid_encoding?
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 = handler.match(pattern))
22
+ if (match_data = whole_regexp.match(pattern))
23
23
  match_data
24
24
  elsif pattern != '/' && pattern.end_with?('/')
25
- handler.match(pattern[0..-2])
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.
@@ -6,7 +6,7 @@
6
6
  #
7
7
  module Padrino
8
8
  # The version constant for the current version of Padrino.
9
- VERSION = '0.18.2' unless defined?(Padrino::VERSION)
9
+ VERSION = '0.18.4' unless defined?(Padrino::VERSION)
10
10
 
11
11
  #
12
12
  # The current Padrino version.