roda 3.107.0 → 3.109.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.
Files changed (105) hide show
  1. checksums.yaml +4 -4
  2. data/lib/roda/opts.rb +22 -0
  3. data/lib/roda/plugins/Integer_matcher_max.rb +1 -1
  4. data/lib/roda/plugins/_add_script_name.rb +14 -0
  5. data/lib/roda/plugins/_base64.rb +2 -2
  6. data/lib/roda/plugins/_json.rb +22 -0
  7. data/lib/roda/plugins/_optimized_matching.rb +1 -0
  8. data/lib/roda/plugins/_symbol_class_matchers.rb +6 -6
  9. data/lib/roda/plugins/additional_render_engines.rb +5 -1
  10. data/lib/roda/plugins/additional_view_directories.rb +5 -1
  11. data/lib/roda/plugins/all_verbs.rb +2 -2
  12. data/lib/roda/plugins/assets.rb +11 -14
  13. data/lib/roda/plugins/autoload_hash_branches.rb +9 -3
  14. data/lib/roda/plugins/autoload_named_routes.rb +9 -3
  15. data/lib/roda/plugins/bearer_token.rb +2 -2
  16. data/lib/roda/plugins/break.rb +9 -1
  17. data/lib/roda/plugins/caching.rb +12 -12
  18. data/lib/roda/plugins/capture_erb.rb +5 -1
  19. data/lib/roda/plugins/chunked.rb +11 -5
  20. data/lib/roda/plugins/class_level_routing.rb +5 -3
  21. data/lib/roda/plugins/class_matchers.rb +3 -1
  22. data/lib/roda/plugins/common_logger.rb +11 -7
  23. data/lib/roda/plugins/conditional_sessions.rb +1 -1
  24. data/lib/roda/plugins/content_for.rb +5 -1
  25. data/lib/roda/plugins/content_security_policy.rb +6 -2
  26. data/lib/roda/plugins/cookie_flags.rb +8 -4
  27. data/lib/roda/plugins/cookies.rb +7 -3
  28. data/lib/roda/plugins/custom_block_results.rb +5 -3
  29. data/lib/roda/plugins/custom_matchers.rb +4 -2
  30. data/lib/roda/plugins/default_headers.rb +2 -5
  31. data/lib/roda/plugins/default_status.rb +1 -0
  32. data/lib/roda/plugins/each_part.rb +23 -27
  33. data/lib/roda/plugins/environments.rb +3 -7
  34. data/lib/roda/plugins/error_email.rb +11 -7
  35. data/lib/roda/plugins/error_handler.rb +5 -3
  36. data/lib/roda/plugins/error_mail.rb +6 -2
  37. data/lib/roda/plugins/exception_page.rb +2 -2
  38. data/lib/roda/plugins/h.rb +4 -4
  39. data/lib/roda/plugins/hash_branch_view_subdir.rb +6 -4
  40. data/lib/roda/plugins/hash_branches.rb +15 -9
  41. data/lib/roda/plugins/hash_paths.rb +11 -7
  42. data/lib/roda/plugins/hash_public.rb +13 -9
  43. data/lib/roda/plugins/hash_public_cache.rb +9 -7
  44. data/lib/roda/plugins/hash_routes.rb +5 -3
  45. data/lib/roda/plugins/heartbeat.rb +6 -2
  46. data/lib/roda/plugins/hmac_paths.rb +36 -32
  47. data/lib/roda/plugins/hooks.rb +13 -10
  48. data/lib/roda/plugins/host_authorization.rb +7 -2
  49. data/lib/roda/plugins/host_routing.rb +36 -6
  50. data/lib/roda/plugins/invalid_request_body.rb +22 -4
  51. data/lib/roda/plugins/ip_from_header.rb +5 -1
  52. data/lib/roda/plugins/json.rb +10 -8
  53. data/lib/roda/plugins/json_parser.rb +43 -11
  54. data/lib/roda/plugins/mail_processor.rb +11 -8
  55. data/lib/roda/plugins/mailer.rb +9 -7
  56. data/lib/roda/plugins/match_affix.rb +6 -1
  57. data/lib/roda/plugins/match_hook_args.rb +7 -5
  58. data/lib/roda/plugins/middleware.rb +13 -7
  59. data/lib/roda/plugins/module_include.rb +1 -1
  60. data/lib/roda/plugins/multi_public.rb +6 -2
  61. data/lib/roda/plugins/multi_route.rb +1 -1
  62. data/lib/roda/plugins/multi_run.rb +13 -17
  63. data/lib/roda/plugins/named_routes.rb +8 -6
  64. data/lib/roda/plugins/named_templates.rb +5 -3
  65. data/lib/roda/plugins/optimized_segment_matchers.rb +13 -6
  66. data/lib/roda/plugins/optimized_string_matchers.rb +6 -3
  67. data/lib/roda/plugins/params_capturing.rb +16 -7
  68. data/lib/roda/plugins/params_capturing_restore.rb +72 -0
  69. data/lib/roda/plugins/part.rb +4 -4
  70. data/lib/roda/plugins/pass.rb +4 -1
  71. data/lib/roda/plugins/path.rb +22 -20
  72. data/lib/roda/plugins/path_matchers.rb +5 -1
  73. data/lib/roda/plugins/path_rewriter.rb +8 -5
  74. data/lib/roda/plugins/permissions_policy.rb +7 -3
  75. data/lib/roda/plugins/public.rb +15 -14
  76. data/lib/roda/plugins/recheck_precompiled_assets.rb +1 -1
  77. data/lib/roda/plugins/redirect_http_to_https.rb +6 -3
  78. data/lib/roda/plugins/render.rb +58 -61
  79. data/lib/roda/plugins/render_coverage.rb +12 -8
  80. data/lib/roda/plugins/render_each.rb +2 -2
  81. data/lib/roda/plugins/render_locals.rb +12 -6
  82. data/lib/roda/plugins/request_aref.rb +1 -1
  83. data/lib/roda/plugins/response_content_type.rb +7 -3
  84. data/lib/roda/plugins/route_csrf.rb +42 -5
  85. data/lib/roda/plugins/run_append_slash.rb +12 -4
  86. data/lib/roda/plugins/sec_fetch_site_csrf.rb +11 -4
  87. data/lib/roda/plugins/sessions.rb +17 -13
  88. data/lib/roda/plugins/shape_friendly.rb +7 -5
  89. data/lib/roda/plugins/sinatra_helpers.rb +7 -3
  90. data/lib/roda/plugins/static.rb +3 -0
  91. data/lib/roda/plugins/status_handler.rb +5 -3
  92. data/lib/roda/plugins/strip_path_prefix.rb +4 -2
  93. data/lib/roda/plugins/symbol_matchers.rb +3 -1
  94. data/lib/roda/plugins/symbol_views.rb +1 -1
  95. data/lib/roda/plugins/timestamp_public.rb +8 -4
  96. data/lib/roda/plugins/type_routing.rb +10 -6
  97. data/lib/roda/plugins/typecast_params.rb +2 -2
  98. data/lib/roda/plugins/typecast_params_sized_integers.rb +1 -1
  99. data/lib/roda/plugins/url_escape.rb +2 -14
  100. data/lib/roda/plugins/view_options.rb +3 -5
  101. data/lib/roda/plugins.rb +14 -4
  102. data/lib/roda/request.rb +25 -9
  103. data/lib/roda/version.rb +1 -1
  104. data/lib/roda.rb +32 -21
  105. metadata +7 -3
@@ -32,14 +32,20 @@ class Roda
32
32
  end
33
33
 
34
34
  def self.configure(app, opts=OPTS)
35
- app.opts[:render_locals] = (app.opts[:render_locals] || {}).merge(opts[:render]||{}).freeze
36
- app.opts[:layout_locals] = (app.opts[:layout_locals] || {}).merge(opts[:layout]||{}).freeze
35
+ app.opts[:render_locals] = (app.render_locals || {}).merge(opts[:render]||{}).freeze
36
+ app.opts[:layout_locals] = (app.layout_locals || {}).merge(opts[:layout]||{}).freeze
37
37
  if opts.has_key?(:merge)
38
38
  app.opts[:merge_locals] = opts[:merge]
39
- app.opts[:layout_locals] = app.opts[:render_locals].merge(app.opts[:layout_locals]).freeze
39
+ app.opts[:layout_locals] = app.render_locals.merge(app.layout_locals).freeze
40
40
  end
41
41
  end
42
42
 
43
+ module ClassMethods
44
+ RodaPlugins.opt_attr_reader(self, :render_locals)
45
+ RodaPlugins.opt_attr_reader(self, :layout_locals)
46
+ RodaPlugins.opt_attr_reader(self, :merge_locals, name: :merge_locals?)
47
+ end
48
+
43
49
  module InstanceMethods
44
50
  private
45
51
 
@@ -56,11 +62,11 @@ class Roda
56
62
  end
57
63
 
58
64
  def render_locals
59
- opts[:render_locals]
65
+ self.class.render_locals
60
66
  end
61
67
 
62
68
  def layout_locals
63
- opts[:layout_locals]
69
+ self.class.layout_locals
64
70
  end
65
71
 
66
72
  # If this isn't the layout template, then use the plugin's render locals as the default locals.
@@ -79,7 +85,7 @@ class Roda
79
85
  # If using a layout, then use the plugin's layout locals as the default locals.
80
86
  def view_layout_opts(opts)
81
87
  if layout_opts = super
82
- merge_locals = layout_opts.has_key?(:merge_locals) ? layout_opts[:merge_locals] : self.opts[:merge_locals]
88
+ merge_locals = layout_opts.fetch(:merge_locals){self.class.merge_locals?}
83
89
 
84
90
  locals = {}
85
91
  locals.merge!(layout_locals)
@@ -20,7 +20,7 @@ class Roda
20
20
  def self.configure(app, setting)
21
21
  case setting
22
22
  when :allow, :raise, :warn
23
- app::RodaRequest.class_eval do
23
+ app::RodaRequest.class_exec do
24
24
  alias_method(:[], :"request_aref_#{setting}")
25
25
  alias_method(:[]=, :"request_aset_#{setting}")
26
26
  public :[], :[]=
@@ -50,12 +50,16 @@ class Roda
50
50
  else
51
51
  mime_types.dup
52
52
  end
53
- app.opts[:repsonse_content_types] = mime_types.freeze
53
+ app.opts[:response_content_types] = mime_types.freeze
54
54
  else
55
- app.opts[:repsonse_content_types] ||= {}
55
+ app.opts[:response_content_types] ||= {}
56
56
  end
57
57
  end
58
58
 
59
+ module ClassMethods
60
+ RodaPlugins.opt_attr_reader(self, :response_content_types)
61
+ end
62
+
59
63
  module ResponseMethods
60
64
  # Return the content-type of the response. Will be nil if it has
61
65
  # not yet been explicitly set.
@@ -68,7 +72,7 @@ class Roda
68
72
  # type with the given file extension. If the symbol is not
69
73
  # a recognized mime type, raises KeyError.
70
74
  def content_type=(mime_type)
71
- mime_type = roda_class.opts[:repsonse_content_types].fetch(mime_type) if mime_type.is_a?(Symbol)
75
+ mime_type = roda_class.response_content_types.fetch(mime_type) if mime_type.is_a?(Symbol)
72
76
  @headers[RodaResponseHeaders::CONTENT_TYPE] = mime_type
73
77
  end
74
78
  end
@@ -57,13 +57,18 @@ class Roda
57
57
  # :csrf_failure :: The action to taken if a request fails the CSRF check (default: :raise). Options:
58
58
  # :raise :: raise a Roda::RodaPlugins::RouteCsrf::InvalidToken exception
59
59
  # :empty_403 :: return a blank 403 page (rack_csrf's default behavior)
60
- # :clear_session :: Clear the current session
60
+ # :clear_session :: Clear the current session (deprecated)
61
61
  # Proc :: Treated as a routing block, called with request object
62
62
  # :check_header :: Whether the HTTP header should be checked for the token value (default: false).
63
63
  # If true, checks the HTTP header after checking for the form input parameter.
64
64
  # If :only, only checks the HTTP header and doesn't check the form input parameter.
65
65
  # :check_request_methods :: Which request methods require CSRF protection
66
66
  # (default: <tt>['POST', 'DELETE', 'PATCH', 'PUT']</tt>)
67
+ # :exempt_request_methods :: If given, specifies which request methods do not require CSRF protection.
68
+ # It is recommended to set this so an allow list approach is used, as
69
+ # :check_request_methods is the equivalent of a deny list
70
+ # (recommended: <tt>['GET', 'HEAD', 'OPTIONS', 'QUERY']</tt>, which
71
+ # will be the default in Roda 4)
67
72
  # :upgrade_from_rack_csrf_key :: If provided, the session key that should be checked for the
68
73
  # rack_csrf raw token. If the session key is present, the value
69
74
  # will be checked against the submitted token, and if it matches,
@@ -155,6 +160,19 @@ class Roda
155
160
  #
156
161
  # By including random data in the HMAC for all tokens, different tokens are generated
157
162
  # each time, mitigating compression ratio attacks such as BREACH.
163
+ #
164
+ # == REQUEST_METHOD modifications
165
+ #
166
+ # It's possible to use Rack middleware that modify REQUEST_METHOD in the env hash,
167
+ # such that it does not reflect that actual request method. One example of this is
168
+ # Rack::MethodOverride. Using such middleware is generally bad from a security perspective,
169
+ # but if you want to do so anyway, you need to override the request method that the
170
+ # route_csrf plugin will use so it reflects the actual request method. If you are using
171
+ # Rack::MethodOverride, you can use:
172
+ #
173
+ # def csrf_request_method
174
+ # env['rack.methodoverride.original_method'] || super
175
+ # end
158
176
  module RouteCsrf
159
177
  # Default CSRF option values
160
178
  DEFAULTS = {
@@ -165,6 +183,7 @@ class Roda
165
183
  :require_request_specific_tokens => true,
166
184
  :csrf_failure => :raise,
167
185
  :check_header => false,
186
+ # RODA4: Remove and set :exempt_request_methods by default
168
187
  :check_request_methods => %w'POST DELETE PATCH PUT'.freeze.each(&:freeze)
169
188
  }.freeze
170
189
 
@@ -177,7 +196,7 @@ class Roda
177
196
  end
178
197
 
179
198
  def self.configure(app, opts=OPTS, &block)
180
- options = app.opts[:route_csrf] = (app.opts[:route_csrf] || DEFAULTS).merge(opts)
199
+ options = app.opts[:route_csrf] = (app.route_csrf_opts || DEFAULTS).merge(opts)
181
200
  if block || opts[:csrf_failure].is_a?(Proc)
182
201
  if block && opts[:csrf_failure]
183
202
  raise RodaError, "Cannot specify both route_csrf plugin block and :csrf_failure option"
@@ -186,10 +205,17 @@ class Roda
186
205
  options[:csrf_failure] = :csrf_failure_method
187
206
  app.define_roda_method(:_roda_route_csrf_failure, 1, &app.send(:convert_route_block, block))
188
207
  end
208
+ if opts[:csrf_failure] == :clear_session
209
+ RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to the route_csrf plugin is deprecated"
210
+ end
189
211
  options[:env_header] = "HTTP_#{options[:header].to_s.tr('-', '_').upcase}".freeze
190
212
  options.freeze
191
213
  end
192
214
 
215
+ module ClassMethods
216
+ RodaPlugins.opt_attr_reader(self, :route_csrf, name: :route_csrf_opts)
217
+ end
218
+
193
219
  module InstanceMethods
194
220
  # Check that the submitted CSRF token is valid, if the request requires a CSRF token.
195
221
  # If the CSRF token is valid or the request does not require a CSRF token, return nil.
@@ -212,6 +238,7 @@ class Roda
212
238
  headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
213
239
  throw :halt, @_response.finish_with_body([])
214
240
  when :clear_session
241
+ RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to check_csrf! is deprecated"
215
242
  session.clear
216
243
  when :csrf_failure_method
217
244
  @_request.on{_roda_route_csrf_failure(@_request)}
@@ -297,13 +324,22 @@ class Roda
297
324
 
298
325
  private
299
326
 
327
+ # The request method to use to determine whether to perform a CSRF check.
328
+ # defaults to REQUEST_METHOD in the environment, can be overridden if
329
+ # that isn't the actual request method.
330
+ def csrf_request_method
331
+ @_request.env['REQUEST_METHOD']
332
+ end
333
+
300
334
  # Returns error message string if the CSRF token is not valid.
301
335
  # Returns nil if the CSRF token is valid.
302
336
  def csrf_invalid_message(opts)
303
337
  opts = opts.empty? ? csrf_options : csrf_options.merge(opts)
304
- method = request.request_method
338
+ method = csrf_request_method
305
339
 
306
- unless opts[:check_request_methods].include?(method)
340
+ if exempt = opts[:exempt_request_methods]
341
+ return if exempt.include?(method)
342
+ elsif !opts[:check_request_methods].include?(method)
307
343
  return
308
344
  end
309
345
 
@@ -326,6 +362,7 @@ class Roda
326
362
  end
327
363
 
328
364
  if (rack_csrf_key = opts[:upgrade_from_rack_csrf_key]) && (rack_csrf_value = session[rack_csrf_key]) && csrf_compare(rack_csrf_value, encoded_token)
365
+ # RODA4: Remove :upgrade_from_rack_csrf_key support
329
366
  return
330
367
  end
331
368
 
@@ -360,7 +397,7 @@ class Roda
360
397
 
361
398
  # Helper for getting the plugin options.
362
399
  def csrf_options
363
- opts[:route_csrf]
400
+ self.class.route_csrf_opts
364
401
  end
365
402
 
366
403
  # Perform a constant-time comparison of the two strings, returning true if they match and false otherwise.
@@ -22,20 +22,28 @@ class Roda
22
22
  # # GET /a/ => App gets "/" as PATH_INFO
23
23
  module RunAppendSlash
24
24
  # Set plugin specific options. Options:
25
- # :use_redirects :: Whether to issue 302 redirects when appending the
26
- # trailing slash.
25
+ # :use_redirects :: Whether to issue a redirect to the path with the slash
26
+ # appended, instead of calling the app directly with the
27
+ # trailing slash. Note that if the current path starts
28
+ # with +//+, it will call the app directly instead of
29
+ # redirecting.
27
30
  def self.configure(app, opts=OPTS)
28
31
  app.opts[:run_append_slash_redirect] = !!opts[:use_redirects]
29
32
  end
30
33
 
34
+ module ClassMethods
35
+ RodaPlugins.opt_attr_reader(self, :run_append_slash_redirect, name: :run_append_slash_redirect?)
36
+ end
37
+
31
38
  module RequestMethods
32
39
  # Calls the given rack app. If the path matches the root of the app but
33
40
  # does not contain a trailing slash, a trailing slash is appended to the
34
41
  # path internally, or a redirect is issued when configured with
35
- # <tt>use_redirects: true</tt>.
42
+ # <tt>use_redirects: true</tt>. If the path starts with +//+,
43
+ # the redirect approach will not be used.
36
44
  def run(*)
37
45
  if @remaining_path.empty?
38
- if scope.opts[:run_append_slash_redirect]
46
+ if roda_class.run_append_slash_redirect? && !(path = self.path).start_with?('//')
39
47
  redirect("#{path}/")
40
48
  else
41
49
  @remaining_path += '/'
@@ -54,7 +54,7 @@ class Roda
54
54
  # :raise :: raise a Roda::RodaPlugins::SecFetchSiteCsrf::CsrfFailure
55
55
  # exception
56
56
  # :empty_403 :: return a blank 403 page
57
- # :clear_session :: clear the current session
57
+ # :clear_session :: clear the current session (deprecated)
58
58
  #
59
59
  # The plugin also supports a block, in which case failures will call the block
60
60
  # as a routing block (the block should accept the request object).
@@ -69,7 +69,7 @@ class Roda
69
69
  class CsrfFailure < RodaError; end
70
70
 
71
71
  def self.configure(app, opts=OPTS, &block)
72
- options = app.opts[:sec_fetch_site_csrf] = (app.opts[:sec_fetch_site_csrf] || DEFAULTS).merge(opts)
72
+ options = app.opts[:sec_fetch_site_csrf] = (app.sec_fetch_site_csrf_opts || DEFAULTS).merge(opts)
73
73
 
74
74
  allowed_values = options[:allowed_values] = ["same-origin"]
75
75
  allowed_values << "same-site" if opts[:allow_same_site]
@@ -83,8 +83,10 @@ class Roda
83
83
  end
84
84
 
85
85
  case options[:csrf_failure]
86
- when :raise, :empty_403, :clear_session, :method
86
+ when :raise, :empty_403, :method
87
87
  # nothing
88
+ when :clear_session
89
+ RodaPlugins.warn "Passing :clear_session as the :csrf_failure option to the sec_fetch_site_csrf plugin is deprecated"
88
90
  else
89
91
  raise RodaError, "Unsupported :csrf_failure plugin option: #{options[:csrf_failure].inspect}"
90
92
  end
@@ -92,6 +94,10 @@ class Roda
92
94
  options.freeze
93
95
  end
94
96
 
97
+ module ClassMethods
98
+ RodaPlugins.opt_attr_reader(self, :sec_fetch_site_csrf, name: :sec_fetch_site_csrf_opts)
99
+ end
100
+
95
101
  module InstanceMethods
96
102
  # Check that the Sec-Fetch-Site header is valid, if the request requires it.
97
103
  # If the header is valid or the request does not require the header, return nil.
@@ -99,7 +105,7 @@ class Roda
99
105
  # if a block is not given, use the plugin :csrf_failure option to determine how to
100
106
  # handle it.
101
107
  def check_sec_fetch_site!(&block)
102
- plugin_opts = self.class.opts[:sec_fetch_site_csrf]
108
+ plugin_opts = self.class.sec_fetch_site_csrf_opts
103
109
  return unless plugin_opts[:check_request_methods].include?(request.request_method)
104
110
 
105
111
  sec_fetch_site = env["HTTP_SEC_FETCH_SITE"]
@@ -118,6 +124,7 @@ class Roda
118
124
  headers[RodaResponseHeaders::CONTENT_LENGTH] ='0'
119
125
  throw :halt, @_response.finish_with_body([])
120
126
  when :clear_session
127
+ # RODA4: Remove
121
128
  session.clear
122
129
  else # when :method
123
130
  @_request.on{_roda_sec_fetch_site_csrf_failure(@_request)}
@@ -5,12 +5,11 @@ require 'openssl'
5
5
  begin
6
6
  OpenSSL::Cipher.new("aes-256-ctr")
7
7
  rescue OpenSSL::Cipher::CipherError
8
- # :nocov:
8
+ # simplecov:disable
9
9
  raise LoadError, "Roda sessions plugin requires the aes-256-ctr cipher"
10
- # :nocov:
10
+ # simplecov:enable
11
11
  end
12
12
 
13
- require 'json'
14
13
  require 'securerandom'
15
14
  require 'zlib'
16
15
  require 'rack/utils'
@@ -175,15 +174,16 @@ class Roda
175
174
 
176
175
  def self.load_dependencies(app, opts=OPTS)
177
176
  app.plugin :_base64
177
+ app.plugin :_json
178
178
  end
179
179
 
180
180
  # Configure the plugin, see Sessions for details on options.
181
181
  def self.configure(app, opts=OPTS)
182
- opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts)
182
+ opts = (app.sessions_opts || DEFAULT_OPTIONS).merge(opts)
183
183
  co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze
184
184
  opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0))
185
- opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse)
186
- opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc
185
+ opts[:parser] ||= app.json_parser
186
+ opts[:serializer] ||= app.json_serializer
187
187
 
188
188
  opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret)
189
189
  opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0
@@ -216,6 +216,10 @@ class Roda
216
216
  app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols)
217
217
  end
218
218
 
219
+ module ClassMethods
220
+ RodaPlugins.opt_attr_reader(self, :sessions, name: :sessions_opts)
221
+ end
222
+
219
223
  module InstanceMethods
220
224
  # Clear data from the session, and update the request environment
221
225
  # so that the session cookie will use a new creation timestamp
@@ -233,7 +237,7 @@ class Roda
233
237
  # update the rack response headers to set the session cookie in
234
238
  # the response.
235
239
  def _roda_after_50__sessions(res)
236
- if res && (session = env[self.class.opts[:sessions][:env_key]])
240
+ if res && (session = env[self.class.sessions_opts[:env_key]])
237
241
  @_request.persist_session(res[1], session)
238
242
  end
239
243
  end
@@ -247,7 +251,7 @@ class Roda
247
251
  # this method stores the session in 'rack.session' in the request environment,
248
252
  # but that does not happen until this method is called.
249
253
  def session
250
- @env[roda_class.opts[:sessions][:env_key]] ||= _load_session
254
+ @env[roda_class.sessions_opts[:env_key]] ||= _load_session
251
255
  end
252
256
 
253
257
  # The time the session was originally created. nil if there is no active session.
@@ -266,7 +270,7 @@ class Roda
266
270
  # Rack::Session::Cookie, mark the related cookie for expiration so it isn't
267
271
  # sent in the future.
268
272
  def persist_session(headers, session)
269
- opts = roda_class.opts[:sessions]
273
+ opts = roda_class.sessions_opts
270
274
 
271
275
  if session.empty?
272
276
  if env[SESSION_SERIALIZED]
@@ -317,7 +321,7 @@ class Roda
317
321
  # Load the session by looking for the appropriate cookie, or falling
318
322
  # back to the rack session cookie if configured.
319
323
  def _load_session
320
- opts = roda_class.opts[:sessions]
324
+ opts = roda_class.sessions_opts
321
325
  cs = cookies
322
326
 
323
327
  if data = cs[opts[:key]]
@@ -340,7 +344,7 @@ class Roda
340
344
  # serialized session using the default Rack::Session::Cookie
341
345
  # hmac and coder.
342
346
  def _deserialize_rack_session(data)
343
- opts = roda_class.opts[:sessions]
347
+ opts = roda_class.sessions_opts
344
348
  data, digest = data.split("--", 2)
345
349
  unless digest
346
350
  return _session_serialization_error("Not decoding Rack::Session::Cookie session: invalid format")
@@ -374,7 +378,7 @@ class Roda
374
378
 
375
379
  # Interpret given cookie data as a Rack::Session::Cookie
376
380
  def _deserialize_session(data)
377
- opts = roda_class.opts[:sessions]
381
+ opts = roda_class.sessions_opts
378
382
 
379
383
  begin
380
384
  data = Base64_.urlsafe_decode64(data)
@@ -466,7 +470,7 @@ class Roda
466
470
  end
467
471
 
468
472
  def _serialize_session(session)
469
- opts = roda_class.opts[:sessions]
473
+ opts = roda_class.sessions_opts
470
474
  env = @env
471
475
  now = Time.now.to_i
472
476
  json_data = opts[:serializer].call(session).force_encoding('BINARY')
@@ -74,6 +74,8 @@ class Roda
74
74
  end
75
75
 
76
76
  module ClassMethods
77
+ RodaPlugins.opt_attr_reader(self, :shape_friendly_scope_instance_variables)
78
+
77
79
  # Automatically refresh the instance variables used if the plugin
78
80
  # sets instance variables.
79
81
  def plugin(plugin, *args, &block)
@@ -90,9 +92,9 @@ class Roda
90
92
  end
91
93
  nil
92
94
  end
93
- # :nocov:
95
+ # simplecov:disable
94
96
  ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
95
- # :nocov:
97
+ # simplecov:enable
96
98
 
97
99
  private
98
100
 
@@ -107,7 +109,7 @@ class Roda
107
109
  ivs.concat(mod.const_get(const)) if mod.const_defined?(const)
108
110
  end
109
111
 
110
- if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs = opts[:shape_friendly_scope_instance_variables])
112
+ if const == :SCOPE_INSTANCE_VARIABLES && (scope_ivs = shape_friendly_scope_instance_variables)
111
113
  ivs.concat(scope_ivs)
112
114
  end
113
115
 
@@ -132,9 +134,9 @@ class Roda
132
134
  end
133
135
  end
134
136
 
135
- # :nocov:
137
+ # simplecov:disable
136
138
  if RUBY_VERSION >= '4.0'
137
- # :nocov:
139
+ # simplecov:enable
138
140
  module InstanceMethods
139
141
  private
140
142
 
@@ -256,6 +256,11 @@ class Roda
256
256
  end
257
257
  end
258
258
 
259
+ module ClassMethods
260
+ RodaPlugins.opt_attr_reader(self, :absolute_redirects, name: :absolute_redirects?)
261
+ RodaPlugins.opt_attr_reader(self, :prefixed_redirects, name: :prefixed_redirects?)
262
+ end
263
+
259
264
  module RequestMethods
260
265
  # Alias for referrer
261
266
  def back
@@ -285,9 +290,8 @@ class Roda
285
290
  # If the absolute_redirects or :prefixed_redirects roda class options has been set, respect those
286
291
  # and update the path.
287
292
  def redirect(path=(no_add_script_name = true; default_redirect_path), status=default_redirect_status)
288
- opts = roda_class.opts
289
- absolute_redirects = opts[:absolute_redirects]
290
- prefixed_redirects = no_add_script_name ? false : opts[:prefixed_redirects]
293
+ absolute_redirects = roda_class.absolute_redirects?
294
+ prefixed_redirects = no_add_script_name ? false : roda_class.prefixed_redirects?
291
295
  path = uri(path, absolute_redirects, prefixed_redirects) if absolute_redirects || prefixed_redirects
292
296
  super(path, status)
293
297
  end
@@ -21,6 +21,9 @@ class Roda
21
21
  # uses a separate option for it.
22
22
  #
23
23
  # Users of this plugin may want to consider using the public plugin instead.
24
+ # As this only loads a middleware, it does not integrate with other Roda
25
+ # security features, such as the content_security_policy and host_authorization
26
+ # plugins.
24
27
  #
25
28
  # Examples:
26
29
  #
@@ -37,6 +37,8 @@ class Roda
37
37
  end
38
38
 
39
39
  module ClassMethods
40
+ RodaPlugins.opt_attr_reader(self, :status_handler, name: :status_handlers)
41
+
40
42
  # Install the given block as a status handler for the given HTTP response code.
41
43
  def status_handler(code, opts=OPTS, &block)
42
44
  # For backwards compatibility, pass request argument if block accepts argument
@@ -54,7 +56,7 @@ class Roda
54
56
  end
55
57
 
56
58
  meth = define_roda_method(:"_roda_status_handler__#{code}", arity, &block)
57
- self.opts[:status_handler][code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result|
59
+ status_handlers[code] = define_roda_method(:"_roda_status_handler_#{code}", 1) do |result|
58
60
  res = @_response
59
61
  res.status = result[0]
60
62
  handle_headers.call(res.headers)
@@ -64,7 +66,7 @@ class Roda
64
66
 
65
67
  # Freeze the hash of status handlers so that there can be no thread safety issues at runtime.
66
68
  def freeze
67
- opts[:status_handler].freeze
69
+ status_handlers.freeze
68
70
  super
69
71
  end
70
72
  end
@@ -74,7 +76,7 @@ class Roda
74
76
 
75
77
  # If routing returns a response we have a handler for, call that handler.
76
78
  def _roda_after_20__status_handler(result)
77
- if result && (meth = opts[:status_handler][result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
79
+ if result && (meth = self.class.status_handlers[result[0]]) && (v = result[2]).is_a?(Array) && v.empty?
78
80
  send(meth, result)
79
81
  end
80
82
  end
@@ -21,9 +21,11 @@ class Roda
21
21
  end
22
22
 
23
23
  module ClassMethods
24
+ RodaPlugins.opt_attr_reader(self, :strip_path_prefix, name: :strip_path_prefix_regexp)
25
+
24
26
  # Strip the path prefix from the gien path if it starts with the prefix.
25
- def expand_path(path, root=opts[:root])
26
- super.sub(opts[:strip_path_prefix], '')
27
+ def expand_path(path, root=app_root)
28
+ super.sub(strip_path_prefix_regexp, '')
27
29
  end
28
30
  end
29
31
  end
@@ -121,6 +121,8 @@ class Roda
121
121
  end
122
122
 
123
123
  module ClassMethods
124
+ RodaPlugins.opt_attr_reader(self, :symbol_matchers)
125
+
124
126
  # Set the matcher and block to use for the given class.
125
127
  # The matcher can be a regexp, registered symbol matcher, or registered class
126
128
  # matcher (if using the class_matchers plugin).
@@ -141,7 +143,7 @@ class Roda
141
143
 
142
144
  # Freeze the class_matchers hash when freezing the app.
143
145
  def freeze
144
- opts[:symbol_matchers].freeze
146
+ symbol_matchers.freeze
145
147
  super
146
148
  end
147
149
  end
@@ -28,7 +28,7 @@ class Roda
28
28
  end
29
29
 
30
30
  def self.configure(app)
31
- app.opts[:custom_block_results][Symbol] = :view
31
+ app.custom_block_results[Symbol] = :view
32
32
  end
33
33
  end
34
34
 
@@ -46,7 +46,11 @@ class Roda
46
46
  #
47
47
  # The options given are also passed to the public plugin.
48
48
  def self.configure(app, opts=OPTS)
49
- app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.opts[:timestamp_public_prefix] || "static").dup.freeze
49
+ app.opts[:timestamp_public_prefix] = (opts[:prefix] || app.timestamp_public_prefix || "static").dup.freeze
50
+ end
51
+
52
+ module ClassMethods
53
+ RodaPlugins.opt_attr_reader(self, :timestamp_public_prefix)
50
54
  end
51
55
 
52
56
  module InstanceMethods
@@ -54,8 +58,8 @@ class Roda
54
58
  # This does not check the file is inside the directory for performance reasons,
55
59
  # so this should not be called with untrusted input.
56
60
  def timestamp_path(file)
57
- mtime = File.mtime(File.join(opts[:public_root], file))
58
- "/#{opts[:timestamp_public_prefix]}/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/#{file}"
61
+ mtime = File.mtime(File.join(self.class.public_root, file))
62
+ "/#{self.class.timestamp_public_prefix}/#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/#{file}"
59
63
  end
60
64
  end
61
65
 
@@ -65,7 +69,7 @@ class Roda
65
69
  # a integer segment for the timestamp, and this is a GET request.
66
70
  def timestamp_public
67
71
  if is_get?
68
- on roda_class.opts[:timestamp_public_prefix], Integer do |_|
72
+ on roda_class.timestamp_public_prefix, Integer do |_|
69
73
  public
70
74
  end
71
75
  end
@@ -110,7 +110,7 @@ class Roda
110
110
  }.freeze
111
111
 
112
112
  def self.configure(app, opts = {})
113
- config = (app.opts[:type_routing] || CONFIGURATION).dup
113
+ config = (app.type_routing_opts || CONFIGURATION).dup
114
114
  [:use_extension, :use_header, :default_type].each do |key|
115
115
  config[key] = opts[key] if opts.has_key?(key)
116
116
  end
@@ -149,12 +149,16 @@ class Roda
149
149
  app.opts[:type_routing] = config.freeze
150
150
  end
151
151
 
152
+ module ClassMethods
153
+ RodaPlugins.opt_attr_reader(self, :type_routing, name: :type_routing_opts)
154
+ end
155
+
152
156
  module RequestMethods
153
157
  # Yields if the given +type+ matches the requested data type and halts
154
158
  # the request afterwards, returning the result of the block.
155
159
  def on_type(type, &block)
156
160
  return unless type == requested_type
157
- response[RodaResponseHeaders::CONTENT_TYPE] ||= @scope.opts[:type_routing][:types][type]
161
+ response[RodaResponseHeaders::CONTENT_TYPE] ||= roda_class.type_routing_opts[:types][type]
158
162
  always(&block)
159
163
  end
160
164
 
@@ -162,7 +166,7 @@ class Roda
162
166
  def requested_type
163
167
  return @requested_type if @requested_type
164
168
 
165
- opts = @scope.opts[:type_routing]
169
+ opts = roda_class.type_routing_opts
166
170
  @requested_type = accept_response_type if opts[:use_header]
167
171
  @requested_type ||= opts[:default_type]
168
172
  end
@@ -182,7 +186,7 @@ class Roda
182
186
  # Removes a trailing file extension from the path, and sets
183
187
  # the requested type if so.
184
188
  def _remaining_path(env)
185
- opts = scope.opts[:type_routing]
189
+ opts = roda_class.type_routing_opts
186
190
  path = super
187
191
 
188
192
  if opts[:use_extension]
@@ -197,12 +201,12 @@ class Roda
197
201
 
198
202
  # The response type indicated by the Accept request header.
199
203
  def accept_response_type
200
- mimes = @scope.opts[:type_routing][:mimes]
204
+ mimes = roda_class.type_routing_opts[:mimes]
205
+ response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
201
206
 
202
207
  @env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part|
203
208
  mime, _= part.split(/\s*;\s*/, 2)
204
209
  if sym = mimes[mime]
205
- response[RodaResponseHeaders::VARY] = (vary = response[RodaResponseHeaders::VARY]) ? "#{vary}, Accept" : 'Accept'
206
210
  return sym
207
211
  end
208
212
  end