roda 3.84.0 → 3.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/roda/plugins/Integer_matcher_max.rb +9 -8
- data/lib/roda/plugins/_optimized_matching.rb +2 -2
- data/lib/roda/plugins/_symbol_class_matchers.rb +107 -0
- data/lib/roda/plugins/autoload_hash_branches.rb +1 -1
- data/lib/roda/plugins/autoload_named_routes.rb +1 -1
- data/lib/roda/plugins/capture_erb.rb +6 -5
- data/lib/roda/plugins/class_matchers.rb +91 -14
- data/lib/roda/plugins/conditional_sessions.rb +67 -0
- data/lib/roda/plugins/content_security_policy.rb +12 -2
- data/lib/roda/plugins/early_hints.rb +1 -2
- data/lib/roda/plugins/permissions_policy.rb +12 -2
- data/lib/roda/plugins/placeholder_string_matchers.rb +4 -0
- data/lib/roda/plugins/public.rb +1 -1
- data/lib/roda/plugins/render.rb +1 -1
- data/lib/roda/plugins/symbol_matchers.rb +70 -15
- data/lib/roda/request.rb +16 -13
- data/lib/roda/version.rb +1 -1
- data/lib/roda.rb +7 -0
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1851a201539b728f1af90ea1b85b2b33a9026f71986cb65c1aabdd079f653ad
|
|
4
|
+
data.tar.gz: f360e0bfeb3442df2fa1f96e28362eee9850c0f54d160bfbfc2b11d62d33ba39
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d2bef4abc3d5e08ddb5a1c9c27f8b626b631a5951cec7cee062c497074cb7f68e6c8b36e75261cc913a580a87d5111438e8a21488669812c9a3b227bf9609e0b
|
|
7
|
+
data.tar.gz: e668a47039e529aa026e21ddfd6346b3e1fa224f432b46085b33242c9551ced88278328f2d2d471b2593f841da0a882d8f85dd2468277484409f679485a219df
|
|
@@ -23,27 +23,28 @@ class Roda
|
|
|
23
23
|
module IntegerMatcherMax
|
|
24
24
|
def self.configure(app, max=nil)
|
|
25
25
|
if max
|
|
26
|
-
app
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
app.class_eval do
|
|
27
|
+
meth = :_max_value_convert_class_Integer
|
|
28
|
+
define_method(meth){max}
|
|
29
|
+
alias_method meth, meth
|
|
30
|
+
private meth
|
|
30
31
|
end
|
|
31
32
|
end
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
module
|
|
35
|
+
module InstanceMethods
|
|
35
36
|
private
|
|
36
37
|
|
|
37
38
|
# Do not have the Integer matcher max when over the maximum
|
|
38
39
|
# configured Integer value.
|
|
39
|
-
def
|
|
40
|
+
def _convert_class_Integer(value)
|
|
40
41
|
value = super
|
|
41
|
-
value if value <=
|
|
42
|
+
value if value <= _max_value_convert_class_Integer
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
# Use 2**63-1 as the default maximum value for the Integer
|
|
45
46
|
# matcher.
|
|
46
|
-
def
|
|
47
|
+
def _max_value_convert_class_Integer
|
|
47
48
|
9223372036854775807
|
|
48
49
|
end
|
|
49
50
|
end
|
|
@@ -52,7 +52,7 @@ class Roda
|
|
|
52
52
|
end
|
|
53
53
|
end
|
|
54
54
|
elsif matcher == Integer
|
|
55
|
-
if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value =
|
|
55
|
+
if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1]))
|
|
56
56
|
@remaining_path = matchdata.post_match
|
|
57
57
|
always{yield(value)}
|
|
58
58
|
end
|
|
@@ -151,7 +151,7 @@ class Roda
|
|
|
151
151
|
always{yield rp[1, len]}
|
|
152
152
|
end
|
|
153
153
|
elsif matcher == Integer
|
|
154
|
-
if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value =
|
|
154
|
+
if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1]))
|
|
155
155
|
@remaining_path = ''
|
|
156
156
|
always{yield(value)}
|
|
157
157
|
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
#
|
|
4
|
+
class Roda
|
|
5
|
+
module RodaPlugins
|
|
6
|
+
module SymbolClassMatchers_
|
|
7
|
+
module ClassMethods
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
# Backend of symbol_matcher and class_matcher.
|
|
11
|
+
def _symbol_class_matcher(expected_class, obj, matcher, block, &request_class_block)
|
|
12
|
+
unless obj.is_a?(expected_class)
|
|
13
|
+
raise RodaError, "Invalid type passed to class_matcher or symbol_matcher: #{matcher.inspect}"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if obj.is_a?(Symbol)
|
|
17
|
+
type = "symbol"
|
|
18
|
+
meth = :"match_symbol_#{obj}"
|
|
19
|
+
else
|
|
20
|
+
type = "class"
|
|
21
|
+
meth = :"_match_class_#{obj}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
case matcher
|
|
25
|
+
when Regexp
|
|
26
|
+
regexp = matcher
|
|
27
|
+
consume_regexp = self::RodaRequest.send(:consume_pattern, regexp)
|
|
28
|
+
when Symbol
|
|
29
|
+
unless opts[:symbol_matchers]
|
|
30
|
+
raise RodaError, "cannot provide Symbol matcher to class_matcher unless using symbol_matchers plugin: #{matcher.inspect}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
regexp, consume_regexp, matcher_block = opts[:symbol_matchers][matcher]
|
|
34
|
+
|
|
35
|
+
unless regexp
|
|
36
|
+
raise RodaError, "unregistered symbol matcher given to #{type}_matcher: #{matcher.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
block = _merge_matcher_blocks(type, obj, block, matcher_block)
|
|
40
|
+
when Class
|
|
41
|
+
unless opts[:class_matchers]
|
|
42
|
+
raise RodaError, "cannot provide Class matcher to symbol_matcher unless using class_matchers plugin: #{matcher.inspect}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
regexp, consume_regexp, matcher_block = opts[:class_matchers][matcher]
|
|
46
|
+
unless regexp
|
|
47
|
+
raise RodaError, "unregistered class matcher given to #{type}_matcher: #{matcher.inspect}"
|
|
48
|
+
end
|
|
49
|
+
block = _merge_matcher_blocks(type, obj, block, matcher_block)
|
|
50
|
+
else
|
|
51
|
+
raise RodaError, "unsupported matcher given to #{type}_matcher: #{matcher.inspect}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if block.is_a?(Symbol)
|
|
55
|
+
convert_meth = block
|
|
56
|
+
elsif block
|
|
57
|
+
convert_meth = :"_convert_#{type}_#{obj}"
|
|
58
|
+
define_method(convert_meth, &block)
|
|
59
|
+
private convert_meth
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
array = opts[:"#{type}_matchers"][obj] = [regexp, consume_regexp, convert_meth].freeze
|
|
63
|
+
|
|
64
|
+
self::RodaRequest.class_eval do
|
|
65
|
+
class_exec(meth, array, &request_class_block)
|
|
66
|
+
private meth
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# If both block and matche_meth are given,
|
|
73
|
+
# define a method for block, and then return a
|
|
74
|
+
# proc that calls matcher_meth first, and only calls
|
|
75
|
+
# the newly defined method with the return values of matcher_meth
|
|
76
|
+
# if matcher_method returns a truthy value.
|
|
77
|
+
# Otherwise, return matcher_meth or block.
|
|
78
|
+
def _merge_matcher_blocks(type, obj, block, matcher_meth)
|
|
79
|
+
if matcher_meth
|
|
80
|
+
if block
|
|
81
|
+
convert_meth = :"_convert_merge_#{type}_#{obj}"
|
|
82
|
+
define_method(convert_meth, &block)
|
|
83
|
+
private convert_meth
|
|
84
|
+
|
|
85
|
+
proc do |*a|
|
|
86
|
+
if captures = send(matcher_meth, *a)
|
|
87
|
+
if captures.is_a?(Array)
|
|
88
|
+
send(convert_meth, *captures)
|
|
89
|
+
else
|
|
90
|
+
send(convert_meth, captures)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
matcher_meth
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
block
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
register_plugin(:_symbol_class_matchers, SymbolClassMatchers_)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
@@ -68,7 +68,7 @@ class Roda
|
|
|
68
68
|
|
|
69
69
|
# Eagerly load all hash branches when freezing the application.
|
|
70
70
|
def freeze
|
|
71
|
-
opts.delete(:autoload_hash_branch_files).each{|file| require file}
|
|
71
|
+
opts.delete(:autoload_hash_branch_files).each{|file| require file} unless opts.frozen?
|
|
72
72
|
super
|
|
73
73
|
end
|
|
74
74
|
end
|
|
@@ -54,7 +54,7 @@ class Roda
|
|
|
54
54
|
|
|
55
55
|
# Eagerly load all autoloaded named routes when freezing the application.
|
|
56
56
|
def freeze
|
|
57
|
-
opts.delete(:autoload_named_route_files).each{|file| require file}
|
|
57
|
+
opts.delete(:autoload_named_route_files).each{|file| require file} unless opts.frozen?
|
|
58
58
|
super
|
|
59
59
|
end
|
|
60
60
|
end
|
|
@@ -16,10 +16,11 @@ class Roda
|
|
|
16
16
|
# to wrap template blocks with arbitrary output and then inject the
|
|
17
17
|
# wrapped output into the template.
|
|
18
18
|
#
|
|
19
|
-
# If the output buffer object responds to +capture+
|
|
20
|
-
# +erubi/capture_block+ is being
|
|
21
|
-
# this will call +capture+ on the
|
|
22
|
-
# of setting the output buffer object
|
|
19
|
+
# If the output buffer object responds to +capture+ and is not
|
|
20
|
+
# an instance of String (e.g. when +erubi/capture_block+ is being
|
|
21
|
+
# used as the template engine), this will call +capture+ on the
|
|
22
|
+
# output buffer object, instead of setting the output buffer object
|
|
23
|
+
# temporarily to a new object.
|
|
23
24
|
module CaptureERB
|
|
24
25
|
def self.load_dependencies(app)
|
|
25
26
|
app.plugin :render
|
|
@@ -34,7 +35,7 @@ class Roda
|
|
|
34
35
|
outvar = render_opts[:template_opts][:outvar]
|
|
35
36
|
buf_was = instance_variable_get(outvar)
|
|
36
37
|
|
|
37
|
-
if buf_was.respond_to?(:capture)
|
|
38
|
+
if buf_was.respond_to?(:capture) && !buf_was.instance_of?(String)
|
|
38
39
|
buf_was.capture(&block)
|
|
39
40
|
else
|
|
40
41
|
begin
|
|
@@ -12,11 +12,10 @@ class Roda
|
|
|
12
12
|
# # ...
|
|
13
13
|
# end
|
|
14
14
|
#
|
|
15
|
-
# You can register a Date class matcher for that regexp
|
|
16
|
-
# the block must return an array):
|
|
15
|
+
# You can register a Date class matcher for that regexp:
|
|
17
16
|
#
|
|
18
17
|
# class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
|
|
19
|
-
#
|
|
18
|
+
# Date.new(y.to_i, m.to_i, d.to_i)
|
|
20
19
|
# end
|
|
21
20
|
#
|
|
22
21
|
# And then use the Date class as a matcher, and it will yield a Date object:
|
|
@@ -26,7 +25,8 @@ class Roda
|
|
|
26
25
|
# end
|
|
27
26
|
#
|
|
28
27
|
# This is useful to DRY up code if you are using the same type of pattern and
|
|
29
|
-
# type conversion in multiple places in your application.
|
|
28
|
+
# type conversion in multiple places in your application. You can have the
|
|
29
|
+
# block return an array to yield multiple captures.
|
|
30
30
|
#
|
|
31
31
|
# If you have a segment match the passed regexp, but decide during block
|
|
32
32
|
# processing that you do not want to treat it as a match, you can have the
|
|
@@ -37,22 +37,99 @@ class Roda
|
|
|
37
37
|
# y = y.to_i
|
|
38
38
|
# m = m.to_i
|
|
39
39
|
# d = d.to_i
|
|
40
|
-
#
|
|
40
|
+
# Date.new(y, m, d) if Date.valid_date?(y, m, d)
|
|
41
41
|
# end
|
|
42
42
|
#
|
|
43
|
+
# The second argument to class_matcher can be a class already registered
|
|
44
|
+
# as a class matcher. This can DRY up code that wants a conversion
|
|
45
|
+
# performed by an existing class matcher:
|
|
46
|
+
#
|
|
47
|
+
# class_matcher Employee, Integer do |id|
|
|
48
|
+
# Employee[id]
|
|
49
|
+
# end
|
|
50
|
+
#
|
|
51
|
+
# With the above example, the Integer matcher performs the conversion to
|
|
52
|
+
# integer, so +id+ is yielded as an integer. The block then looks up the
|
|
53
|
+
# employee with that id. If there is no employee with that id, then
|
|
54
|
+
# the Employee matcher will not match.
|
|
55
|
+
#
|
|
56
|
+
# If using the symbol_matchers plugin, you can provide a recognized symbol
|
|
57
|
+
# matcher as the second argument to class_matcher, and it will work in
|
|
58
|
+
# a similar manner:
|
|
59
|
+
#
|
|
60
|
+
# symbol_matcher(:employee_id, /E-(\d{6})/) do |employee_id|
|
|
61
|
+
# employee_id.to_i
|
|
62
|
+
# end
|
|
63
|
+
# class_matcher Employee, :employee_id do |id|
|
|
64
|
+
# Employee[id]
|
|
65
|
+
# end
|
|
66
|
+
#
|
|
67
|
+
# Blocks passed to the class_matchers plugin are evaluated in route
|
|
68
|
+
# block context.
|
|
69
|
+
#
|
|
43
70
|
# This plugin does not work with the params_capturing plugin, as it does not
|
|
44
71
|
# offer the ability to associate block arguments with named keys.
|
|
45
72
|
module ClassMatchers
|
|
73
|
+
def self.load_dependencies(app)
|
|
74
|
+
app.plugin :_symbol_class_matchers
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.configure(app)
|
|
78
|
+
app.opts[:class_matchers] ||= {
|
|
79
|
+
Integer=>[/(\d{1,100})/, /\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer].freeze,
|
|
80
|
+
String=>[/([^\/]+)/, nil, nil].freeze
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
46
84
|
module ClassMethods
|
|
47
|
-
# Set the
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
85
|
+
# Set the matcher and block to use for the given class.
|
|
86
|
+
# The matcher can be a regexp, registered class matcher, or registered symbol
|
|
87
|
+
# matcher (if using the symbol_matchers plugin).
|
|
88
|
+
#
|
|
89
|
+
# If providing a regexp, the block given will be called with all regexp captures.
|
|
90
|
+
# If providing a registered class or symbol, the block will be called with the
|
|
91
|
+
# captures returned by the block for the registered class or symbol, or the regexp
|
|
92
|
+
# captures if no block was registered with the class or symbol. In either case,
|
|
93
|
+
# if a block is given, it should return an array with the captures to yield to
|
|
94
|
+
# the match block.
|
|
95
|
+
def class_matcher(klass, matcher, &block)
|
|
96
|
+
_symbol_class_matcher(Class, klass, matcher, block) do |meth, (_, regexp, convert_meth)|
|
|
97
|
+
if regexp
|
|
98
|
+
define_method(meth){consume(regexp, convert_meth)}
|
|
99
|
+
else
|
|
100
|
+
define_method(meth){_consume_segment(convert_meth)}
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Freeze the class_matchers hash when freezing the app.
|
|
106
|
+
def freeze
|
|
107
|
+
opts[:class_matchers].freeze
|
|
108
|
+
super
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
module RequestMethods
|
|
113
|
+
# Use faster approach for segment matching. This is used for
|
|
114
|
+
# matchers based on the String class matcher, and avoids the
|
|
115
|
+
# use of regular expressions for scanning.
|
|
116
|
+
def _consume_segment(convert_meth)
|
|
117
|
+
rp = @remaining_path
|
|
118
|
+
if _match_class_String
|
|
119
|
+
if convert_meth
|
|
120
|
+
if captures = scope.send(convert_meth, @captures.pop)
|
|
121
|
+
if captures.is_a?(Array)
|
|
122
|
+
@captures.concat(captures)
|
|
123
|
+
else
|
|
124
|
+
@captures << captures
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
@remaining_path = rp
|
|
128
|
+
nil
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
true
|
|
132
|
+
end
|
|
56
133
|
end
|
|
57
134
|
end
|
|
58
135
|
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen-string-literal: true
|
|
2
|
+
|
|
3
|
+
class Roda
|
|
4
|
+
module RodaPlugins
|
|
5
|
+
# The conditional_sessions plugin loads the sessions plugin. However,
|
|
6
|
+
# it only allows sessions if the block passed to the plugin returns
|
|
7
|
+
# truthy. The block is evaluated in request context. This is designed for
|
|
8
|
+
# use in applications that want to use sessions for some requests,
|
|
9
|
+
# and want to be sure that sessions are not used for other requests.
|
|
10
|
+
# For example, if you want to make sure that sessions are not used for
|
|
11
|
+
# requests with paths starting with /static, you could do:
|
|
12
|
+
#
|
|
13
|
+
# plugin :conditional_sessions, secret: ENV["SECRET"] do
|
|
14
|
+
# !path_info.start_with?('/static')
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# The the request session, session_created_at, and session_updated_at methods
|
|
18
|
+
# raise a RodaError exception when sessions are not allowed. The request
|
|
19
|
+
# persist_session and route scope clear_session methods do nothing when
|
|
20
|
+
# sessions are not allowed.
|
|
21
|
+
module ConditionalSessions
|
|
22
|
+
# Pass all options to the sessions block, and use the block to define
|
|
23
|
+
# a request method for whether sessions are allowed.
|
|
24
|
+
def self.load_dependencies(app, opts=OPTS, &block)
|
|
25
|
+
app.plugin :sessions, opts
|
|
26
|
+
app::RodaRequest.class_eval do
|
|
27
|
+
define_method(:use_sessions?, &block)
|
|
28
|
+
alias use_sessions? use_sessions?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module InstanceMethods
|
|
33
|
+
# Do nothing if not using sessions.
|
|
34
|
+
def clear_session
|
|
35
|
+
super if @_request.use_sessions?
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
module RequestMethods
|
|
40
|
+
# Raise RodaError if not using sessions.
|
|
41
|
+
def session
|
|
42
|
+
raise RodaError, "session called on request not using sessions" unless use_sessions?
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Raise RodaError if not using sessions.
|
|
47
|
+
def session_created_at
|
|
48
|
+
raise RodaError, "session_created_at called on request not using sessions" unless use_sessions?
|
|
49
|
+
super
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Raise RodaError if not using sessions.
|
|
53
|
+
def session_updated_at
|
|
54
|
+
raise RodaError, "session_updated_at called on request not using sessions" unless use_sessions?
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Do nothing if not using sessions.
|
|
59
|
+
def persist_session(headers, session)
|
|
60
|
+
super if use_sessions?
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
register_plugin(:conditional_sessions, ConditionalSessions)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -92,7 +92,10 @@ class Roda
|
|
|
92
92
|
# content_security_policy.get_script_src
|
|
93
93
|
# # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']]
|
|
94
94
|
#
|
|
95
|
-
# The clear method can be used to remove all settings from the policy.
|
|
95
|
+
# The clear method can be used to remove all settings from the policy. Empty policies
|
|
96
|
+
# do not set any headers. You can use +response.skip_content_security_policy!+ to skip
|
|
97
|
+
# setting a policy. This is faster than calling +content_security_policy.clear+, since
|
|
98
|
+
# it does not duplicate the default policy.
|
|
96
99
|
#
|
|
97
100
|
# The following methods to set boolean settings are also defined:
|
|
98
101
|
#
|
|
@@ -304,12 +307,19 @@ class Roda
|
|
|
304
307
|
@content_security_policy ||= roda_class.opts[:content_security_policy].dup
|
|
305
308
|
end
|
|
306
309
|
|
|
310
|
+
# Do not set a content security policy header for this response.
|
|
311
|
+
def skip_content_security_policy!
|
|
312
|
+
@skip_content_security_policy = true
|
|
313
|
+
end
|
|
314
|
+
|
|
307
315
|
private
|
|
308
316
|
|
|
309
317
|
# Set the appropriate content security policy header.
|
|
310
318
|
def set_default_headers
|
|
311
319
|
super
|
|
312
|
-
|
|
320
|
+
unless @skip_content_security_policy
|
|
321
|
+
(@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
|
|
322
|
+
end
|
|
313
323
|
end
|
|
314
324
|
end
|
|
315
325
|
end
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
class Roda
|
|
5
5
|
module RodaPlugins
|
|
6
6
|
# The early_hints plugin allows sending 103 Early Hints responses
|
|
7
|
-
# using the rack.early_hints environment variable.
|
|
8
|
-
# is only supported by puma 3.11+, and on other servers this is a no-op.
|
|
7
|
+
# using the rack.early_hints environment variable.
|
|
9
8
|
# Early hints allow clients to preload necessary files before receiving
|
|
10
9
|
# the response.
|
|
11
10
|
module EarlyHints
|
|
@@ -99,7 +99,10 @@ class Roda
|
|
|
99
99
|
# permissions_policy.get_fullscreen
|
|
100
100
|
# # => [:self, "https://example.com", "https://*.example.com"]
|
|
101
101
|
#
|
|
102
|
-
# The clear method can be used to remove all settings from the policy.
|
|
102
|
+
# The clear method can be used to remove all settings from the policy. Empty policies
|
|
103
|
+
# do not set any headers. You can use +response.skip_permissions_policy!+ to skip
|
|
104
|
+
# setting a policy. This is faster than calling +permissions_policy.clear+, since
|
|
105
|
+
# it does not duplicate the default policy.
|
|
103
106
|
module PermissionsPolicy
|
|
104
107
|
SUPPORTED_SETTINGS = %w'
|
|
105
108
|
accelerometer
|
|
@@ -311,12 +314,19 @@ class Roda
|
|
|
311
314
|
@permissions_policy ||= roda_class.opts[:permissions_policy].dup
|
|
312
315
|
end
|
|
313
316
|
|
|
317
|
+
# Do not set a permissions policy header for this response.
|
|
318
|
+
def skip_permissions_policy!
|
|
319
|
+
@skip_permissions_policy = true
|
|
320
|
+
end
|
|
321
|
+
|
|
314
322
|
private
|
|
315
323
|
|
|
316
324
|
# Set the appropriate permissions policy header.
|
|
317
325
|
def set_default_headers
|
|
318
326
|
super
|
|
319
|
-
|
|
327
|
+
unless @skip_permissions_policy
|
|
328
|
+
(@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
|
|
329
|
+
end
|
|
320
330
|
end
|
|
321
331
|
end
|
|
322
332
|
end
|
|
@@ -21,6 +21,10 @@ class Roda
|
|
|
21
21
|
#
|
|
22
22
|
# r.is "foo", String
|
|
23
23
|
# r.is "foo", :bar
|
|
24
|
+
#
|
|
25
|
+
# If used with the symbol_matchers plugin, this plugin respects the regexps
|
|
26
|
+
# for the registered symbols, but it does not perform the conversions, the
|
|
27
|
+
# captures for the regexp are used directly as the captures for the match method.
|
|
24
28
|
module PlaceholderStringMatchers
|
|
25
29
|
def self.load_dependencies(app)
|
|
26
30
|
app.plugin :_symbol_regexp_matchers
|
data/lib/roda/plugins/public.rb
CHANGED
|
@@ -42,12 +42,12 @@ class Roda
|
|
|
42
42
|
# end
|
|
43
43
|
module Public
|
|
44
44
|
SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
|
|
45
|
-
PARSER = URI::DEFAULT_PARSER
|
|
46
45
|
RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
|
|
47
46
|
ENCODING_MAP = {:zstd=>'zstd', :brotli=>'br', :gzip=>'gzip'}.freeze
|
|
48
47
|
ENCODING_EXTENSIONS = {'br'=>'.br', 'gzip'=>'.gz', 'zstd'=>'.zst'}.freeze
|
|
49
48
|
|
|
50
49
|
# :nocov:
|
|
50
|
+
PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
|
|
51
51
|
MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
|
|
52
52
|
# :nocov:
|
|
53
53
|
|
data/lib/roda/plugins/render.rb
CHANGED
|
@@ -48,7 +48,7 @@ class Roda
|
|
|
48
48
|
#
|
|
49
49
|
# :allowed_paths :: Set the template paths to allow. Attempts to render paths outside
|
|
50
50
|
# of these paths will raise an error. Defaults to the +:views+ directory.
|
|
51
|
-
# :cache :: nil/false to explicitly disable
|
|
51
|
+
# :cache :: nil/false to explicitly disable permanent template caching. By default, permanent
|
|
52
52
|
# template caching is disabled by default if RACK_ENV is development. When permanent
|
|
53
53
|
# template caching is disabled, for templates with paths in the file system, the
|
|
54
54
|
# modification time of the file will be checked on every render, and if it has changed,
|
|
@@ -23,7 +23,7 @@ class Roda
|
|
|
23
23
|
#
|
|
24
24
|
# :d :: <tt>/(\d+)/</tt>, a decimal segment
|
|
25
25
|
# :rest :: <tt>/(.*)/</tt>, all remaining characters, if any
|
|
26
|
-
# :w :: <tt>/(\w+)/</tt>,
|
|
26
|
+
# :w :: <tt>/(\w+)/</tt>, an alphanumeric segment
|
|
27
27
|
#
|
|
28
28
|
# If the placeholder_string_matchers plugin is loaded, this feature also applies to
|
|
29
29
|
# placeholders in strings, so the following:
|
|
@@ -39,11 +39,10 @@ class Roda
|
|
|
39
39
|
# be loaded first.
|
|
40
40
|
#
|
|
41
41
|
# You can provide a block when calling +symbol_matcher+, and it will be called
|
|
42
|
-
# for all matches to allow for type conversion
|
|
43
|
-
# array:
|
|
42
|
+
# for all matches to allow for type conversion:
|
|
44
43
|
#
|
|
45
44
|
# symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
|
|
46
|
-
#
|
|
45
|
+
# Date.new(y.to_i, m.to_i, d.to_i)
|
|
47
46
|
# end
|
|
48
47
|
#
|
|
49
48
|
# route do |r|
|
|
@@ -61,29 +60,80 @@ class Roda
|
|
|
61
60
|
# y = y.to_i
|
|
62
61
|
# m = m.to_i
|
|
63
62
|
# d = d.to_i
|
|
64
|
-
#
|
|
63
|
+
# Date.new(y, m, d) if Date.valid_date?(y, m, d)
|
|
65
64
|
# end
|
|
66
65
|
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
66
|
+
# You can have the block return an array to yield multiple captures.
|
|
67
|
+
#
|
|
68
|
+
# The second argument to symbol_matcher can be a symbol already registered
|
|
69
|
+
# as a symbol matcher. This can DRY up code that wants a conversion
|
|
70
|
+
# performed by an existing class matcher or to use the same regexp:
|
|
71
|
+
#
|
|
72
|
+
# symbol_matcher :employee_id, :d do |id|
|
|
73
|
+
# id.to_i
|
|
74
|
+
# end
|
|
75
|
+
# symbol_matcher :employee, :employee_id do |id|
|
|
76
|
+
# Employee[id]
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
# With the above example, the :d matcher matches only decimal strings, but
|
|
80
|
+
# yields them as string. The registered :employee_id matcher converts the
|
|
81
|
+
# decimal string to an integer. The registered :employee matcher builds
|
|
82
|
+
# on that and uses the integer to lookup the related employee. If there is
|
|
83
|
+
# no employee with that id, then the :employee matcher will not match.
|
|
84
|
+
#
|
|
85
|
+
# If using the class_matchers plugin, you can provide a recognized class
|
|
86
|
+
# matcher as the second argument to symbol_matcher, and it will work in
|
|
87
|
+
# a similar manner:
|
|
88
|
+
#
|
|
89
|
+
# symbol_matcher :employee, Integer do |id|
|
|
90
|
+
# Employee[id]
|
|
91
|
+
# end
|
|
92
|
+
#
|
|
93
|
+
# Blocks passed to the symbol matchers plugin are evaluated in route
|
|
94
|
+
# block context.
|
|
95
|
+
#
|
|
96
|
+
# If providing a block to the symbol_matchers plugin, the symbol may
|
|
97
|
+
# not work with the params_capturing plugin. Note that the use of
|
|
98
|
+
# symbol matchers inside strings when using the placeholder_string_matchers
|
|
99
|
+
# plugin only uses the regexp, it does not respect the conversion blocks
|
|
100
|
+
# registered with the symbols.
|
|
69
101
|
module SymbolMatchers
|
|
70
102
|
def self.load_dependencies(app)
|
|
71
103
|
app.plugin :_symbol_regexp_matchers
|
|
104
|
+
app.plugin :_symbol_class_matchers
|
|
72
105
|
end
|
|
73
106
|
|
|
74
107
|
def self.configure(app)
|
|
108
|
+
app.opts[:symbol_matchers] ||= {}
|
|
75
109
|
app.symbol_matcher(:d, /(\d+)/)
|
|
76
110
|
app.symbol_matcher(:w, /(\w+)/)
|
|
77
111
|
app.symbol_matcher(:rest, /(.*)/)
|
|
78
112
|
end
|
|
79
113
|
|
|
80
114
|
module ClassMethods
|
|
81
|
-
# Set the
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
115
|
+
# Set the matcher and block to use for the given class.
|
|
116
|
+
# The matcher can be a regexp, registered symbol matcher, or registered class
|
|
117
|
+
# matcher (if using the class_matchers plugin).
|
|
118
|
+
#
|
|
119
|
+
# If providing a regexp, the block given will be called with all regexp captures.
|
|
120
|
+
# If providing a registered symbol or class, the block will be called with the
|
|
121
|
+
# captures returned by the block for the registered symbol or class, or the regexp
|
|
122
|
+
# captures if no block was registered with the symbol or class. In either case,
|
|
123
|
+
# if a block is given, it should return an array with the captures to yield to
|
|
124
|
+
# the match block.
|
|
125
|
+
def symbol_matcher(s, matcher, &block)
|
|
126
|
+
_symbol_class_matcher(Symbol, s, matcher, block) do |meth, array|
|
|
127
|
+
define_method(meth){array}
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Freeze the class_matchers hash when freezing the app.
|
|
134
|
+
def freeze
|
|
135
|
+
opts[:symbol_matchers].freeze
|
|
136
|
+
super
|
|
87
137
|
end
|
|
88
138
|
end
|
|
89
139
|
|
|
@@ -97,8 +147,13 @@ class Roda
|
|
|
97
147
|
meth = :"match_symbol_#{s}"
|
|
98
148
|
if respond_to?(meth, true)
|
|
99
149
|
# Allow calling private match methods
|
|
100
|
-
re,
|
|
101
|
-
|
|
150
|
+
_, re, convert_meth = send(meth)
|
|
151
|
+
if re
|
|
152
|
+
consume(re, convert_meth)
|
|
153
|
+
else
|
|
154
|
+
# defined in class_matchers plugin
|
|
155
|
+
_consume_segment(convert_meth)
|
|
156
|
+
end
|
|
102
157
|
else
|
|
103
158
|
super
|
|
104
159
|
end
|
data/lib/roda/request.rb
CHANGED
|
@@ -443,16 +443,7 @@ class Roda
|
|
|
443
443
|
# Match integer segment of up to 100 decimal characters, and yield resulting value as an
|
|
444
444
|
# integer.
|
|
445
445
|
def _match_class_Integer
|
|
446
|
-
consume(/\A\/(\d{1,100})(?=\/|\z)
|
|
447
|
-
if i = _match_class_convert_Integer(i)
|
|
448
|
-
[i]
|
|
449
|
-
end
|
|
450
|
-
end
|
|
451
|
-
end
|
|
452
|
-
|
|
453
|
-
# Convert the segment matched by the Integer matcher to an integer.
|
|
454
|
-
def _match_class_convert_Integer(value)
|
|
455
|
-
value.to_i
|
|
446
|
+
consume(/\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer)
|
|
456
447
|
end
|
|
457
448
|
|
|
458
449
|
# Match only if all of the arguments in the given array match.
|
|
@@ -555,14 +546,26 @@ class Roda
|
|
|
555
546
|
# match, returns false without changes. Otherwise, modifies
|
|
556
547
|
# SCRIPT_NAME to include the matched path, removes the matched
|
|
557
548
|
# path from PATH_INFO, and updates captures with any regex captures.
|
|
558
|
-
def consume(pattern)
|
|
549
|
+
def consume(pattern, meth=nil)
|
|
559
550
|
if matchdata = pattern.match(@remaining_path)
|
|
560
551
|
captures = matchdata.captures
|
|
561
|
-
|
|
552
|
+
|
|
553
|
+
if meth
|
|
554
|
+
return unless captures = scope.send(meth, *captures)
|
|
555
|
+
# :nocov:
|
|
556
|
+
elsif defined?(yield)
|
|
557
|
+
# RODA4: Remove
|
|
562
558
|
return unless captures = yield(*captures)
|
|
559
|
+
# :nocov:
|
|
563
560
|
end
|
|
561
|
+
|
|
564
562
|
@remaining_path = matchdata.post_match
|
|
565
|
-
|
|
563
|
+
|
|
564
|
+
if captures.is_a?(Array)
|
|
565
|
+
@captures.concat(captures)
|
|
566
|
+
else
|
|
567
|
+
@captures << captures
|
|
568
|
+
end
|
|
566
569
|
end
|
|
567
570
|
end
|
|
568
571
|
|
data/lib/roda/version.rb
CHANGED
data/lib/roda.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: roda
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.86.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jeremy Evans
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2024-
|
|
11
|
+
date: 2024-11-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rack
|
|
@@ -167,6 +167,7 @@ files:
|
|
|
167
167
|
- lib/roda/plugins/_base64.rb
|
|
168
168
|
- lib/roda/plugins/_before_hook.rb
|
|
169
169
|
- lib/roda/plugins/_optimized_matching.rb
|
|
170
|
+
- lib/roda/plugins/_symbol_class_matchers.rb
|
|
170
171
|
- lib/roda/plugins/_symbol_regexp_matchers.rb
|
|
171
172
|
- lib/roda/plugins/additional_render_engines.rb
|
|
172
173
|
- lib/roda/plugins/additional_view_directories.rb
|
|
@@ -185,6 +186,7 @@ files:
|
|
|
185
186
|
- lib/roda/plugins/class_level_routing.rb
|
|
186
187
|
- lib/roda/plugins/class_matchers.rb
|
|
187
188
|
- lib/roda/plugins/common_logger.rb
|
|
189
|
+
- lib/roda/plugins/conditional_sessions.rb
|
|
188
190
|
- lib/roda/plugins/content_for.rb
|
|
189
191
|
- lib/roda/plugins/content_security_policy.rb
|
|
190
192
|
- lib/roda/plugins/cookie_flags.rb
|
|
@@ -325,7 +327,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
325
327
|
- !ruby/object:Gem::Version
|
|
326
328
|
version: '0'
|
|
327
329
|
requirements: []
|
|
328
|
-
rubygems_version: 3.5.
|
|
330
|
+
rubygems_version: 3.5.22
|
|
329
331
|
signing_key:
|
|
330
332
|
specification_version: 4
|
|
331
333
|
summary: Routing tree web toolkit
|