roda 3.85.0 → 3.87.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a862ed0414bf90081e744ed7c50ac19806f141ebeae8c699d5f96b9a4e718e10
4
- data.tar.gz: b1fcd622255d24044ba7f26c2c3f35e5fc8ea7f30b2ba577bec5fe306ca75c76
3
+ metadata.gz: bee7ae126ad0e6e8081bba94be0ad60e2a5f7523c483b2441571549b1bd29274
4
+ data.tar.gz: f5441e96617877d1347f4a5b6e54e5d48713ffc91a8ad3b9282781c0c26ca108
5
5
  SHA512:
6
- metadata.gz: '086d21f564d66b14fafabbb99c6d670559c12fed91105c1acb9e5e9147623bf15de0730d35f2b5bd098a6e26aba0d6a73497a914ee23d97d8cb58dbbf0f6dd5b'
7
- data.tar.gz: 713dee3419ce7bf52742d7da6be4243c393b393aeb5762dcd0dc61a58d691e586226636b5df687e56e0dd280a89930a18b1e32f3c9b499516afc461cf92fcf4c
6
+ metadata.gz: 141408b4d85e7467c0b753c05180201b1a63a3096f713241b94d379bad8d65224d96f1d60978ac336a7d8b9fb3093f35c1d2887d422276cf255cd1c64c557719
7
+ data.tar.gz: a64207e6d0a46d22c59bc0acc9d56ed43f4dbc75a9d22737908097564a2e4e801f3c19fd3bf95a28722dbf4a828d9930146dd088e421847709948d3a8f2cf24b
@@ -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
@@ -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
- (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
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
@@ -28,7 +28,16 @@ class Roda
28
28
  #
29
29
  # Note that custom block result handling only occurs if the types
30
30
  # are not handled by Roda itself. You cannot use this to modify
31
- # the handling of nil, false, or string results.
31
+ # the handling of nil, false, or string results. Additionally,
32
+ # if the response body has already been written to before the the
33
+ # route block exits, then the result of the block is ignored,
34
+ # and the related +handle_block_result+ block will not be called
35
+ # (this is standard Roda behavior).
36
+ #
37
+ # The return value of the +handle_block_result+ block is written
38
+ # to the body if the block return value is a String, similar to
39
+ # standard Roda handling of block results. Non-String return
40
+ # values are ignored.
32
41
  module CustomBlockResults
33
42
  def self.configure(app)
34
43
  app.opts[:custom_block_results] ||= {}
@@ -55,7 +64,15 @@ class Roda
55
64
  # to get the block result.
56
65
  def unsupported_block_result(result)
57
66
  roda_class.opts[:custom_block_results].each do |klass, meth|
58
- return scope.send(meth, result) if klass === result
67
+ if klass === result
68
+ result = scope.send(meth, result)
69
+
70
+ if String === result
71
+ return result
72
+ else
73
+ return
74
+ end
75
+ end
59
76
  end
60
77
 
61
78
  super
@@ -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. Currently, this
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
@@ -51,7 +51,8 @@ class Roda
51
51
 
52
52
  # Match if the given uppercase key is present inside the environment.
53
53
  def match_header(key)
54
- key = key.upcase.tr("-","_")
54
+ key = key.upcase
55
+ key.tr!("-","_")
55
56
  unless key == "CONTENT_TYPE" || key == "CONTENT_LENGTH"
56
57
  key = "HTTP_#{key}"
57
58
  end
@@ -75,8 +76,8 @@ class Roda
75
76
  # Match the submitted user agent to the given pattern, capturing any
76
77
  # regexp match groups.
77
78
  def match_user_agent(pattern)
78
- if (user_agent = @env["HTTP_USER_AGENT"]) && user_agent.to_s =~ pattern
79
- @captures.concat($~[1..-1])
79
+ if (user_agent = @env["HTTP_USER_AGENT"]) && (match = pattern.match(user_agent))
80
+ @captures.concat(match.captures)
80
81
  end
81
82
  end
82
83
  end
@@ -0,0 +1,190 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The host_routing plugin adds support for more routing requests based on
7
+ # the requested host. It also adds predicate methods for checking
8
+ # whether a request was requested with the given host.
9
+ #
10
+ # When loading the plugin, you pass a block, which is used for configuring
11
+ # the plugin. For example, if you want to treat requests to api.example.com
12
+ # or api2.example.com as api requests, and treat other requests as www
13
+ # requests, you could use:
14
+ #
15
+ # plugin :host_routing do |hosts|
16
+ # hosts.to :api, "api.example.com", "api2.example.com"
17
+ # hosts.default :www
18
+ # end
19
+ #
20
+ # With this configuration, in your routing tree, you can call the +r.api+ and
21
+ # +r.www+ methods for dispatching to routing blocks only for those types of
22
+ # requests:
23
+ #
24
+ # route do |r|
25
+ # r.api do
26
+ # # requests to api.example.com or api2.example.com
27
+ # end
28
+ #
29
+ # r.www do
30
+ # # requests to other domains
31
+ # end
32
+ # end
33
+ #
34
+ # In addition to the routing methods, predicate methods are also added to the
35
+ # request object:
36
+ #
37
+ # route do |r|
38
+ # "#{r.api?}-#{r.www?}"
39
+ # end
40
+ # # Requests to api.example.com or api2.example.com return "true-false"
41
+ # # Other requests return "false-true"
42
+ #
43
+ # If the +:scope_predicates+ plugin option is given, predicate methods are also
44
+ # created in route block scope:
45
+ #
46
+ # plugin :host_routing, scope_predicates: true do |hosts|
47
+ # hosts.to :api, "api.example.com"
48
+ # hosts.default :www
49
+ # end
50
+ #
51
+ # route do |r|
52
+ # "#{api?}-#{www?}"
53
+ # end
54
+ #
55
+ # To handle hosts that match a certain format (such as all subdomains),
56
+ # where the specific host names are not known up front, you can provide a block
57
+ # when calling +hosts.default+. This block is passed the host name, or an empty
58
+ # string if no host name is provided, and is evaluated in route block scope.
59
+ # When using this support, you should also call +hosts.register+
60
+ # to register host types that could be returned by the block. For example, to
61
+ # handle api subdomains differently:
62
+ #
63
+ # plugin :host_routing do |hosts|
64
+ # hosts.to :api, "api.example.com"
65
+ # hosts.register :api_sub
66
+ # hosts.default :www do |host|
67
+ # :api_sub if host.end_with?(".api.example.com")
68
+ # end
69
+ # end
70
+ #
71
+ # This plugin uses the host method on the request to get the hostname (this method
72
+ # is defined by Rack).
73
+ module HostRouting
74
+ # Setup the host routing support. The block yields an object used to
75
+ # configure the plugin. Options:
76
+ #
77
+ # :scope_predicates :: Setup predicate methods in route block scope
78
+ # in addition to request scope.
79
+ def self.configure(app, opts=OPTS, &block)
80
+ hosts, host_hash, default_block, default_host = DSL.new.process(&block)
81
+ app.opts[:host_routing_hash] = host_hash
82
+ app.opts[:host_routing_default_host] = default_host
83
+
84
+ app.send(:define_method, :_host_routing_default, &default_block) if default_block
85
+
86
+ app::RodaRequest.class_exec do
87
+ hosts.each do |host|
88
+ host_sym = host.to_sym
89
+ define_method(host_sym){|&blk| always(&blk) if _host_routing_host == host}
90
+ alias_method host_sym, host_sym
91
+
92
+ meth = :"#{host}?"
93
+ define_method(meth){_host_routing_host == host}
94
+ alias_method meth, meth
95
+ end
96
+ end
97
+
98
+ if opts[:scope_predicates]
99
+ app.class_exec do
100
+ hosts.each do |host|
101
+ meth = :"#{host}?"
102
+ define_method(meth){@_request.send(meth)}
103
+ alias_method meth, meth
104
+ end
105
+ end
106
+ end
107
+ end
108
+
109
+ class DSL
110
+ def initialize
111
+ @hosts = []
112
+ @host_hash = {}
113
+ end
114
+
115
+ # Run the DSL for the given block.
116
+ def process(&block)
117
+ instance_exec(self, &block)
118
+
119
+ if !@default_host
120
+ raise RodaError, "must call default method inside host_routing plugin block to set default host"
121
+ end
122
+
123
+ @hosts.concat(@host_hash.values)
124
+ @hosts << @default_host
125
+ @hosts.uniq!
126
+ [@hosts.freeze, @host_hash.freeze, @default_block, @default_host].freeze
127
+ end
128
+
129
+ # Register hosts that can be returned. This is only needed if
130
+ # calling register with a block, where the block can return
131
+ # a value that doesn't match a host given to +to+ or +default+.
132
+ def register(*hosts)
133
+ @hosts = hosts
134
+ end
135
+
136
+ # Treat all given hostnames as routing to the give host.
137
+ def to(host, *hostnames)
138
+ hostnames.each do |hostname|
139
+ @host_hash[hostname] = host
140
+ end
141
+ end
142
+
143
+ # Register the default hostname. If a block is provided, it is
144
+ # called with the host if there is no match for one of the hostnames
145
+ # provided to +to+. If the block returns nil/false, the hostname
146
+ # given to this method is used.
147
+ def default(hostname, &block)
148
+ @default_host = hostname
149
+ @default_block = block
150
+ end
151
+ end
152
+ private_constant :DSL
153
+
154
+ module InstanceMethods
155
+ # Handle case where plugin is used without providing a block to
156
+ # +hosts.default+. This returns nil, ensuring that the hostname
157
+ # provided to +hosts.default+ will be used.
158
+ def _host_routing_default(_)
159
+ nil
160
+ end
161
+ end
162
+
163
+ module RequestMethods
164
+ private
165
+
166
+ # Cache the host to use in the host routing support, so the processing
167
+ # is only done once per request.
168
+ def _host_routing_host
169
+ @_host_routing_host ||= _get_host_routing_host
170
+ end
171
+
172
+ # Determine the host to use for the host routing support. Tries the
173
+ # following, in order:
174
+ #
175
+ # * An exact match for a hostname given in +hosts.to+
176
+ # * The return value of the +hosts.default+ block, if given
177
+ # * The default value provided in the +hosts.default+ call
178
+ def _get_host_routing_host
179
+ host = self.host || ""
180
+
181
+ roda_class.opts[:host_routing_hash][host] ||
182
+ scope._host_routing_default(host) ||
183
+ roda_class.opts[:host_routing_default_host]
184
+ end
185
+ end
186
+ end
187
+
188
+ register_plugin(:host_routing, HostRouting)
189
+ end
190
+ end
@@ -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
- (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
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
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 85
7
+ RodaMinorVersion = 87
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.85.0
4
+ version: 3.87.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-10-11 00:00:00.000000000 Z
11
+ date: 2024-12-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -186,6 +186,7 @@ files:
186
186
  - lib/roda/plugins/class_level_routing.rb
187
187
  - lib/roda/plugins/class_matchers.rb
188
188
  - lib/roda/plugins/common_logger.rb
189
+ - lib/roda/plugins/conditional_sessions.rb
189
190
  - lib/roda/plugins/content_for.rb
190
191
  - lib/roda/plugins/content_security_policy.rb
191
192
  - lib/roda/plugins/cookie_flags.rb
@@ -224,6 +225,7 @@ files:
224
225
  - lib/roda/plugins/hmac_paths.rb
225
226
  - lib/roda/plugins/hooks.rb
226
227
  - lib/roda/plugins/host_authorization.rb
228
+ - lib/roda/plugins/host_routing.rb
227
229
  - lib/roda/plugins/hsts.rb
228
230
  - lib/roda/plugins/indifferent_params.rb
229
231
  - lib/roda/plugins/inject_erb.rb
@@ -326,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
326
328
  - !ruby/object:Gem::Version
327
329
  version: '0'
328
330
  requirements: []
329
- rubygems_version: 3.5.16
331
+ rubygems_version: 3.5.22
330
332
  signing_key:
331
333
  specification_version: 4
332
334
  summary: Routing tree web toolkit