roda 3.109.0 → 3.110.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/lib/roda/cache.rb +0 -2
  3. data/lib/roda/opts.rb +1 -1
  4. data/lib/roda/plugins/_symbol_class_matchers.rb +2 -4
  5. data/lib/roda/plugins/additional_view_directories.rb +2 -2
  6. data/lib/roda/plugins/assets.rb +173 -142
  7. data/lib/roda/plugins/assets_preloading.rb +26 -10
  8. data/lib/roda/plugins/backtracking_array.rb +2 -2
  9. data/lib/roda/plugins/branch_locals.rb +4 -4
  10. data/lib/roda/plugins/break.rb +2 -2
  11. data/lib/roda/plugins/caching.rb +27 -17
  12. data/lib/roda/plugins/capture_erb.rb +15 -12
  13. data/lib/roda/plugins/chunked.rb +32 -30
  14. data/lib/roda/plugins/class_level_routing.rb +5 -2
  15. data/lib/roda/plugins/class_matchers.rb +11 -6
  16. data/lib/roda/plugins/common_logger.rb +21 -3
  17. data/lib/roda/plugins/conditional_sessions.rb +3 -3
  18. data/lib/roda/plugins/content_for.rb +7 -6
  19. data/lib/roda/plugins/content_security_policy.rb +11 -12
  20. data/lib/roda/plugins/cookie_flags.rb +12 -6
  21. data/lib/roda/plugins/cookies.rb +7 -5
  22. data/lib/roda/plugins/csrf.rb +2 -1
  23. data/lib/roda/plugins/custom_block_results.rb +6 -7
  24. data/lib/roda/plugins/default_headers.rb +2 -2
  25. data/lib/roda/plugins/delay_build.rb +1 -1
  26. data/lib/roda/plugins/delegate.rb +7 -6
  27. data/lib/roda/plugins/direct_call.rb +5 -1
  28. data/lib/roda/plugins/disallow_file_uploads.rb +2 -3
  29. data/lib/roda/plugins/drop_body.rb +3 -6
  30. data/lib/roda/plugins/each_part.rb +4 -6
  31. data/lib/roda/plugins/early_hints.rb +4 -4
  32. data/lib/roda/plugins/empty_root.rb +5 -5
  33. data/lib/roda/plugins/environments.rb +8 -8
  34. data/lib/roda/plugins/erb_h.rb +4 -4
  35. data/lib/roda/plugins/error_email.rb +16 -17
  36. data/lib/roda/plugins/error_handler.rb +24 -32
  37. data/lib/roda/plugins/error_mail.rb +26 -15
  38. data/lib/roda/plugins/exception_page.rb +41 -35
  39. data/lib/roda/plugins/filter_common_logger.rb +6 -4
  40. data/lib/roda/plugins/flash.rb +16 -13
  41. data/lib/roda/plugins/freeze_request_response.rb +24 -0
  42. data/lib/roda/plugins/hash_routes.rb +1 -1
  43. data/lib/roda/plugins/permissions_policy.rb +14 -16
  44. data/lib/roda/plugins/public.rb +11 -0
  45. data/lib/roda/plugins/recheck_precompiled_assets.rb +30 -8
  46. data/lib/roda/plugins/render.rb +3 -0
  47. data/lib/roda/plugins/render_locals.rb +1 -1
  48. data/lib/roda/plugins/sessions.rb +158 -137
  49. data/lib/roda/plugins/symbol_matchers.rb +8 -5
  50. data/lib/roda/plugins/unescape_path.rb +22 -4
  51. data/lib/roda/plugins.rb +24 -0
  52. data/lib/roda/request.rb +8 -7
  53. data/lib/roda/response.rb +9 -7
  54. data/lib/roda/version.rb +1 -1
  55. data/lib/roda.rb +12 -9
  56. metadata +2 -1
@@ -3,26 +3,19 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The error_handler plugin adds an error handler to the routing,
6
+ # The error_handler plugin adds an error handler to the routing tree,
7
7
  # so that if routing the request raises an error, a nice error
8
- # message page can be returned to the user.
8
+ # message page (or API response) can be returned to the user.
9
9
  #
10
- # You can provide the error handler as a block to the plugin:
10
+ # You should provide the error handler as a block to the plugin:
11
11
  #
12
12
  # plugin :error_handler do |e|
13
13
  # "Oh No!"
14
14
  # end
15
15
  #
16
- # Or later via the +error+ class method:
17
- #
18
- # plugin :error_handler
19
- #
20
- # error do |e|
21
- # "Oh No!"
22
- # end
23
- #
24
- # In both cases, the exception instance is passed into the block,
25
- # and the block can return the request body via a string.
16
+ # If an error occurs, the exception instance is passed into the block,
17
+ # and the block can return the request body via a string (or another
18
+ # configured block result).
26
19
  #
27
20
  # If an exception is raised, a new response will be used, with the
28
21
  # default status set to 500, before executing the error handler.
@@ -32,40 +25,40 @@ class Roda
32
25
  # processing of that response occurs, except that an exception during
33
26
  # after processing is logged to <tt>env['rack.errors']</tt> but
34
27
  # otherwise ignored. This avoids recursive calls into the
35
- # error_handler. Note that if the error_handler itself raises
28
+ # error handler. Note that if the error handler itself raises
36
29
  # an exception, the exception will be raised without normal after
37
30
  # processing. This can cause some after processing to run twice
38
- # (once before the error_handler is called and once after) if
31
+ # (once before the error handler is called and once after) if
39
32
  # later after processing raises an exception.
40
33
  #
41
34
  # By default, this plugin handles StandardError and ScriptError.
42
- # To override the exception classes it will handle, pass a :classes
43
- # option to the plugin:
35
+ # To override the exception classes it will handle, pass a +classes+
36
+ # argument to the plugin:
44
37
  #
45
38
  # plugin :error_handler, classes: [StandardError, ScriptError, NoMemoryError]
46
39
  module ErrorHandler
47
40
  DEFAULT_ERROR_HANDLER_CLASSES = [StandardError, ScriptError].freeze
48
41
 
49
- # If a block is given, automatically call the +error+ method on
50
- # the Roda class with it.
51
- def self.configure(app, opts={}, &block)
52
- app.opts[:error_handler_classes] = (opts[:classes] || app.error_handler_classes || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze
42
+ # If a block is given, use it to setup the error handler.
43
+ def self.configure(app, opts=(no_positional_opts = true; OPTS), classes: opts[:classes], **kw, &block)
44
+ RodaPlugins.check_keywords("the error_handler plugin", no_positional_opts, opts, kw)
45
+ app.opts[:error_handler_classes] = (classes || app.error_handler_classes || DEFAULT_ERROR_HANDLER_CLASSES).dup.freeze
53
46
 
54
47
  if block
55
- app.error(&block)
48
+ app.class_exec do
49
+ define_method(:handle_error, &block)
50
+ alias_method(:handle_error, :handle_error)
51
+ private :handle_error
52
+ end
56
53
  end
57
54
  end
58
55
 
59
56
  module ClassMethods
60
57
  RodaPlugins.opt_attr_reader(self, :error_handler_classes)
61
58
 
62
- # Install the given block as the error handler, so that if routing
63
- # the request raises an exception, the block will be called with
64
- # the exception in the scope of the Roda instance.
65
59
  def error(&block)
66
- define_method(:handle_error, &block)
67
- alias_method(:handle_error, :handle_error)
68
- private :handle_error
60
+ RodaPlugins.warn("The error class method is deprecated, switch to passing the block when loading the error_handler plugin", uplevel: 1)
61
+ plugin(:error_handler, &block)
69
62
  end
70
63
  end
71
64
 
@@ -98,7 +91,7 @@ class Roda
98
91
  private
99
92
 
100
93
  # Default empty implementation of _roda_after, usually
101
- # overridden by Roda.def_roda_before.
94
+ # overridden by Roda.def_roda_after.
102
95
  def _roda_after(res)
103
96
  end
104
97
 
@@ -121,9 +114,8 @@ class Roda
121
114
  res
122
115
  end
123
116
 
124
- # By default, have the error handler reraise the error, so using
125
- # the plugin without installing an error handler doesn't change
126
- # behavior.
117
+ # By default, have the error handler reraise the error, so loading
118
+ # the plugin without a block doesn't change behavior.
127
119
  def handle_error(e)
128
120
  raise e
129
121
  end
@@ -5,8 +5,8 @@ require 'mail'
5
5
  class Roda
6
6
  module RodaPlugins
7
7
  # The error_mail plugin adds an +error_mail+ instance method that
8
- # send an email related to the exception. This is most useful if you are
9
- # also using the error_handler plugin:
8
+ # send an email related to the exception. This is designed to be used
9
+ # with the error_handler plugin:
10
10
  #
11
11
  # plugin :error_mail, to: 'to@example.com', from: 'from@example.com'
12
12
  # plugin :error_handler do |e|
@@ -21,13 +21,13 @@ class Roda
21
21
  #
22
22
  # Options:
23
23
  #
24
- # :filter :: Callable called with the key and value for each parameter, environment
25
- # variable, and session value. If it returns true, the value of the
26
- # parameter is filtered in the email.
27
- # :from :: The From address to use in the email (required)
28
- # :headers :: A hash of additional headers to use in the email (default: empty hash)
29
- # :prefix :: A prefix to use in the email's subject line (default: no prefix)
30
- # :to :: The To address to use in the email (required)
24
+ # +:filter+ :: Callable called with the key and value for each parameter, environment
25
+ # variable, and session value. If it returns true, the value of the
26
+ # parameter is filtered in the email.
27
+ # +:from+ :: The From address to use in the email (required)
28
+ # +:headers+ :: A hash of additional headers to use in the email (default: empty hash)
29
+ # +:prefix+ :: A prefix to use in the email's subject line (default: no prefix)
30
+ # +:to+ :: The To address to use in the email (required)
31
31
  #
32
32
  # The subject of the error email shows the exception class and message.
33
33
  # The body of the error email shows the backtrace of the error and the
@@ -42,16 +42,27 @@ class Roda
42
42
  DEFAULT_FILTER = lambda{|k,v| false}
43
43
  private_constant :DEFAULT_FILTER
44
44
 
45
+ PLUGIN_ARGUMENTS = %i[to from headers prefix filter].freeze
46
+ private_constant :PLUGIN_ARGUMENTS
47
+
48
+ Config = Struct.new(*PLUGIN_ARGUMENTS)
49
+ private_constant :Config
50
+
45
51
  # Set default opts for plugin. See ErrorEmail module RDoc for options.
46
52
  def self.configure(app, opts=OPTS)
53
+ RodaPlugins.check_supported_opts("the error_mail plugin", opts, PLUGIN_ARGUMENTS)
54
+
47
55
  app.opts[:error_mail] = email_opts = (app.error_mail_opts || {:filter=>DEFAULT_FILTER}).merge(opts).freeze
48
56
  unless email_opts[:to] && email_opts[:from]
49
57
  raise RodaError, "must provide :to and :from options to error_mail plugin"
50
58
  end
59
+
60
+ app.opts[:error_mail_config] = Config.new(*email_opts.values_at(*Config.members)).freeze
51
61
  end
52
62
 
53
63
  module ClassMethods
54
64
  RodaPlugins.opt_attr_reader(self, :error_mail, name: :error_mail_opts)
65
+ RodaPlugins.opt_attr_reader(self, :error_mail_config)
55
66
  end
56
67
 
57
68
  module InstanceMethods
@@ -71,14 +82,14 @@ class Roda
71
82
  private
72
83
 
73
84
  def _error_mail(e)
74
- email_opts = self.class.error_mail_opts
85
+ config = self.class.error_mail_config
75
86
  subject = if e.respond_to?(:message)
76
87
  "#{e.class}: #{e.message}"
77
88
  else
78
89
  e.to_s
79
90
  end
80
- subject = "#{email_opts[:prefix]}#{subject}"
81
- filter = email_opts[:filter]
91
+ subject = "#{config.prefix}#{subject}"
92
+ filter = config.filter
82
93
 
83
94
  format = lambda do |h|
84
95
  h = h.map{|k, v| "#{k.inspect} => #{filter.call(k, v) ? 'FILTERED' : v.inspect}"}
@@ -133,12 +144,12 @@ END
133
144
 
134
145
 
135
146
  Mail.new do
136
- from email_opts[:from]
137
- to email_opts[:to]
147
+ from config.from
148
+ to config.to
138
149
  subject subject
139
150
  body message
140
151
 
141
- if headers = email_opts[:headers]
152
+ if headers = config.headers
142
153
  headers.each do |k,v|
143
154
  header[k] = v
144
155
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The exception_page plugin provides an exception_page method that is designed
6
+ # The exception_page plugin provides an +exception_page+ method that is designed
7
7
  # to be called inside the error handler to provide a page to the developer
8
8
  # with debugging information. It should only be used in developer environments
9
9
  # with trusted clients, as it can leak source code and other information that
@@ -21,29 +21,29 @@ class Roda
21
21
  # differences:
22
22
  #
23
23
  # * Not a middleware, so it doesn't handle exceptions itself, and has no effect
24
- # on the callstack unless the exception_page method is called.
24
+ # on the callstack unless the +exception_page+ method is called.
25
25
  # * Supports external javascript and stylesheets, allowing context toggling to
26
26
  # work in applications that use a content security policy to restrict inline
27
- # javascript and stylesheets (:assets, :css_file, and :js_file options).
28
- # * Has fewer dependencies (does not require ostruct and erb).
27
+ # javascript and stylesheets (+assets+, +css_file+, and +js_file+ arguments).
28
+ # * Has fewer dependencies (does not require erb).
29
29
  # * Sets the Content-Type for the response, and returns the body string, but does
30
30
  # not modify other headers or the response status.
31
- # * Supports a configurable amount of context lines in backtraces (:context option).
32
- # * Supports optional JSON formatted output, if used with the json plugin (:json option).
31
+ # * Supports a configurable amount of context lines in backtraces (+context+ argument).
32
+ # * Supports optional JSON formatted output, if used with the json plugin (+json+ argument).
33
33
  #
34
- # To use the external javascript and stylesheets, you can call +r.exception_page_assets+
35
- # in your routing tree:
34
+ # To handle requests for the external javascript and stylesheets, you can call
35
+ # +r.exception_page_assets+ in your routing tree:
36
36
  #
37
37
  # route do |r|
38
38
  # # ...
39
39
  #
40
40
  # # serve GET /exception_page.{css,js} requests
41
- # # Use with assets: true +exception_page+ option
41
+ # # Use with exception_page assets: true argument
42
42
  # r.exception_page_assets
43
43
  #
44
44
  # r.on "static" do
45
45
  # # serve GET /static/exception_page.{css,js} requests
46
- # # Use with assets: '/static' +exception_page+ option
46
+ # # Use with exception_page assets: '/static' option
47
47
  # r.exception_page_assets
48
48
  # end
49
49
  # end
@@ -175,29 +175,37 @@ END
175
175
  # is not accepted, then just show a plain text page with the exception
176
176
  # class, message, and backtrace.
177
177
  #
178
- # Options:
178
+ # Arguments:
179
179
  #
180
- # :assets :: If +true+, sets :css_file to +/exception_page.css+ and :js_file to
181
- # +/exception_page.js+, assuming that +r.exception_page_assets+ is called
182
- # in the route block to serve the exception page assets. If a String,
183
- # uses the string as a prefix, assuming that +r.exception_page_assets+
184
- # is called in a nested block inside the route block. If false, doesn't
185
- # use any CSS or JS.
186
- # :context :: The number of context lines before and after each line in
187
- # the backtrace (default: 7).
188
- # :css_file :: A path to the external CSS file for the HTML exception page. If false,
189
- # doesn't use any CSS.
190
- # :js_file :: A path to the external javascript file for the HTML exception page. If
191
- # false, doesn't use any JS.
192
- # :json :: Return a hash of exception information. The hash will have
193
- # a single key, "exception", with a value being a hash with
194
- # three keys, "class", "message", and "backtrace", which
195
- # contain information derived from the given exception.
196
- # Designed to be used with the +json+ exception, which will
197
- # automatically convert the hash to JSON format.
198
- def exception_page(exception, opts=OPTS)
180
+ # +assets+ :: If +true+, sets :css_file to +/exception_page.css+ and :js_file to
181
+ # +/exception_page.js+, assuming that +r.exception_page_assets+ is called
182
+ # in the route block to serve the exception page assets. If a String,
183
+ # uses the string as a prefix, assuming that +r.exception_page_assets+
184
+ # is called in a nested block inside the route block. If false, doesn't
185
+ # use any CSS or JS.
186
+ # +context+ :: The number of context lines before and after each line in
187
+ # the backtrace (default: 7).
188
+ # +css_file+ :: A path to the external CSS file for the HTML exception page. If false,
189
+ # doesn't use any CSS.
190
+ # +js_file+ :: A path to the external javascript file for the HTML exception page. If
191
+ # false, doesn't use any JS.
192
+ # +json+ :: Return a hash of exception information. The hash will have
193
+ # a single key, <tt>"exception"</tt>, with a value being a hash with
194
+ # three keys, <tt>"class"</tt>, <tt>"message"</tt>, and <tt>"backtrace"</tt>, which
195
+ # contain information derived from the given exception.
196
+ # Designed to be used with the +json+ plugin, which will
197
+ # automatically convert the hash to JSON format.
198
+ def exception_page(exception, opts=(no_positional_opts = true; OPTS),
199
+ assets: opts[:assets],
200
+ context: opts[:context],
201
+ css_file: opts[:css_file],
202
+ js_file: opts[:js_file],
203
+ json: opts[:json],
204
+ **kw)
205
+ RodaPlugins.check_keywords(__callee__, no_positional_opts, opts, kw)
206
+
199
207
  message = exception_page_exception_message(exception)
200
- if opts[:json]
208
+ if json
201
209
  @_response[RodaResponseHeaders::CONTENT_TYPE] = "application/json"
202
210
  {
203
211
  "exception"=>{
@@ -209,11 +217,9 @@ END
209
217
  elsif env['HTTP_ACCEPT'] =~ /text\/html/
210
218
  @_response[RodaResponseHeaders::CONTENT_TYPE] = "text/html"
211
219
 
212
- context = opts[:context] || 7
213
- css_file = opts[:css_file]
214
- js_file = opts[:js_file]
220
+ context ||= 7
215
221
 
216
- case prefix = opts[:assets]
222
+ case prefix = assets
217
223
  when false
218
224
  css_file = false if css_file.nil?
219
225
  js_file = false if js_file.nil?
@@ -26,15 +26,17 @@ class Roda
26
26
  end
27
27
 
28
28
  def self.configure(app, &block)
29
- app.send(:define_method, :_common_log_request?, &block)
30
- app.send(:private, :_common_log_request?)
31
- app.send(:alias_method, :_common_log_request?, :_common_log_request?)
29
+ app.class_exec do
30
+ define_method(:_common_log_request?, &block)
31
+ private :_common_log_request?
32
+ alias _common_log_request? _common_log_request?
33
+ end
32
34
  end
33
35
 
34
36
  module InstanceMethods
35
37
  private
36
38
 
37
- # Log request/response information in common log format to logger.
39
+ # Do common logging only if configured to do so.
38
40
  def _roda_after_90__common_logger(result)
39
41
  super if result && _common_log_request?(result)
40
42
  end
@@ -4,14 +4,12 @@ require 'delegate'
4
4
 
5
5
  class Roda
6
6
  module RodaPlugins
7
- # The flash plugin adds a +flash+ instance method to Roda,
8
- # for typical web application flash handling, where values
9
- # set in the current flash hash are available in the next
10
- # request.
7
+ # The flash plugin adds a +flash+ instance method, for typical web
8
+ # application flash handling, where values set in the current flash
9
+ # hash are available in the next request loading the flash.
11
10
  #
12
- # With the example below, if a POST request is submitted,
13
- # it will redirect and the resulting GET request will
14
- # return 'b'.
11
+ # With the example below, if a +POST+ request is submitted, it will
12
+ # redirect and the resulting +GET+ request will return <tt>'b'</tt>.
15
13
  #
16
14
  # plugin :flash
17
15
  #
@@ -23,7 +21,7 @@ class Roda
23
21
  #
24
22
  # r.post do
25
23
  # flash['a'] = 'b'
26
- # r.redirect('')
24
+ # r.redirect
27
25
  # end
28
26
  # end
29
27
  # end
@@ -35,11 +33,16 @@ class Roda
35
33
  # flash.now['a'] = 'b'
36
34
  # flash['a'] # = >'b'
37
35
  # end
36
+ #
37
+ # The flash is saved in the session, so it has the same restrictions
38
+ # as other data saved in the session. If the session does not support
39
+ # symbols (e.g. when using the sessions plugin), you should not use
40
+ # symbols as flash keys.
38
41
  module Flash
39
42
  SCOPE_INSTANCE_VARIABLES = [:@_flash].freeze
40
43
 
41
- # Simple flash hash, where assiging to the hash updates the flash
42
- # used in the following request.
44
+ # Hash-like flash, where assigning to the object updates the data
45
+ # accessed in the following request.
43
46
  class FlashHash < DelegateClass(Hash)
44
47
  # The flash hash for the next request. This
45
48
  # is what gets written to by #[]=.
@@ -48,8 +51,8 @@ class Roda
48
51
  # The flash hash for the current request
49
52
  alias now __getobj__
50
53
 
51
- # Setup the next hash when initializing, and handle treat nil
52
- # as a new empty hash.
54
+ # Setup the next hash when initializing, and treat nil
55
+ # argument as a new empty hash.
53
56
  def initialize(hash={})
54
57
  super(hash||{})
55
58
  @next = {}
@@ -93,7 +96,7 @@ class Roda
93
96
  # Access the flash hash for the current request, loading
94
97
  # it from the session if it is not already loaded.
95
98
  def flash
96
- # :_flash to support transparent upgrades from previous key
99
+ # RODA4: Remove fallback to checking :_flash
97
100
  @_flash ||= FlashHash.new(session['_flash'] || (session['_flash'] = session.delete(:_flash)))
98
101
  end
99
102
 
@@ -0,0 +1,24 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The freeze_request_response plugin freezes the RodaRequest and
7
+ # RodaResponse classes when freezing the Roda class. This will
8
+ # become the default behavior in Roda 4.
9
+ module FreezeRequestResponse
10
+ module ClassMethods
11
+ # Freeze the RodaRequest and RodaResponse classes when freezing the Roda class.
12
+ def freeze
13
+ super
14
+ # RODA4: make this behavior the default
15
+ self::RodaRequest.freeze
16
+ self::RodaResponse.freeze
17
+ self
18
+ end
19
+ end
20
+ end
21
+
22
+ register_plugin(:freeze_request_response, FreezeRequestResponse)
23
+ end
24
+ end
@@ -241,7 +241,7 @@ class Roda
241
241
  end
242
242
  end
243
243
 
244
- [:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
244
+ %i[get post delete head options link patch put trace unlink query].each do |meth|
245
245
  define_method(meth) do |path, &block|
246
246
  verb(meth, path, &block)
247
247
  end
@@ -34,13 +34,13 @@ class Roda
34
34
  # # ...
35
35
  # end
36
36
  #
37
- # You can use the :default plugin option to set the default for all settings.
37
+ # You can use the +:default+ plugin argument to set the default for all settings.
38
38
  # For example, to disallow all access for each setting by default:
39
39
  #
40
40
  # plugin :permissions_policy, default: :none
41
41
  #
42
42
  # The following methods are available for configuring the permissions policy,
43
- # which specify the setting (substituting _ with -):
43
+ # which specify the setting (substituting +_+ with +-+):
44
44
  #
45
45
  # * accelerometer
46
46
  # * ambient_light_sensor
@@ -73,12 +73,12 @@ class Roda
73
73
  # All of these methods support any number of arguments, and each argument should
74
74
  # be one of the following values:
75
75
  #
76
- # :all :: Grants permission to all domains (must be only argument)
77
- # :none :: Does not allow permission at all (must be only argument)
78
- # :self :: Allows feature in current document and any nested browsing contexts
79
- # that use the same domain as the current document.
80
- # :src :: Allows feature in current document and any nested browsing contexts
81
- # that use the same domain as the src of the iframe.
76
+ # +:all+ :: Grants permission to all domains (must be only argument)
77
+ # +:none+ :: Does not allow permission at all (must be only argument)
78
+ # +:self+ :: Allows feature in current document and any nested browsing contexts
79
+ # that use the same domain as the current document.
80
+ # +:src+ :: Allows feature in current document and any nested browsing contexts
81
+ # that use the same domain as the src of the iframe.
82
82
  # String :: Specifies origin domain where access is allowed
83
83
  #
84
84
  # When calling a method with no arguments, the setting is removed from the policy instead
@@ -88,7 +88,7 @@ class Roda
88
88
  # Calling the method overrides any previous setting. Each of the methods has +add_*+ and
89
89
  # +get_*+ methods defined. The +add_*+ method appends to any existing setting, and the +get_*+ method
90
90
  # returns the current value for the setting (this will be +:all+ if all domains are allowed, or
91
- # any array of strings/:self/:src).
91
+ # any array of strings/+:self+/+:src+).
92
92
  #
93
93
  # permissions_policy.fullscreen :self, 'https://example.com'
94
94
  # # fullscreen (self "https://example.com")
@@ -99,7 +99,7 @@ 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. Empty policies
102
+ # The +clear+ method can be used to remove all settings from the policy. Empty policies
103
103
  # do not set any headers. You can use +response.skip_permissions_policy!+ to skip
104
104
  # setting a policy. This is faster than calling +permissions_policy.clear+, since
105
105
  # it does not duplicate the default policy.
@@ -175,16 +175,13 @@ class Roda
175
175
  end
176
176
  end
177
177
 
178
- def initialize
179
- clear
180
- end
181
-
182
178
  # Clear all settings, useful to remove any inherited settings.
183
179
  def clear
184
180
  @opts = {}
185
181
  @report_only = nil
186
182
  @header_value = nil
187
183
  end
184
+ alias initialize clear
188
185
 
189
186
  # Do not allow future modifications to any settings.
190
187
  def freeze
@@ -279,14 +276,15 @@ class Roda
279
276
  end
280
277
 
281
278
  # Yield the current Permissions Policy to the block.
282
- def self.configure(app, opts=OPTS)
279
+ def self.configure(app, opts=(no_positional_opts = true; OPTS), default: opts[:default], **kw)
280
+ RodaPlugins.check_keywords("the permissions_policy plugin", no_positional_opts, opts, kw)
283
281
  policy = app.opts[:permissions_policy] = if policy = app.permissions_policy
284
282
  policy.dup
285
283
  else
286
284
  Policy.new
287
285
  end
288
286
 
289
- if default = opts[:default]
287
+ if default
290
288
  SUPPORTED_SETTINGS.each do |setting|
291
289
  policy.send(setting.tr('-', '_'), *default)
292
290
  end
@@ -140,10 +140,20 @@ class Roda
140
140
  s, h, b = public_serve(server, path)
141
141
  headers = response.headers
142
142
  headers.replace(h)
143
+ public_add_vary(headers)
143
144
  halt [s, headers, b]
144
145
  end
145
146
  end
146
147
 
148
+ # When encodings are used, the response depends on the Accept-Encoding
149
+ # request header, so add it to the Vary header for caches.
150
+ def public_add_vary(headers)
151
+ unless roda_class.public_encodings.empty?
152
+ headers[RodaResponseHeaders::VARY] = (vary = headers[RodaResponseHeaders::VARY]) ? "#{vary}, Accept-Encoding" : 'Accept-Encoding'
153
+ end
154
+ nil
155
+ end
156
+
147
157
  # Serve the compressed file if it exists. This should only
148
158
  # be called if the client will accept the related encoding.
149
159
  def public_serve_compressed(server, path, suffix, encoding)
@@ -158,6 +168,7 @@ class Roda
158
168
  headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
159
169
  headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding
160
170
  end
171
+ public_add_vary(headers)
161
172
 
162
173
  halt [s, headers, b]
163
174
  end
@@ -48,27 +48,37 @@ class Roda
48
48
  end
49
49
 
50
50
  def self.load_dependencies(app)
51
- unless app.respond_to?(:assets_opts) && app.assets_opts[:precompiled]
51
+ unless app.respond_to?(:assets_config) && app.assets_config.precompiled
52
52
  raise RodaError, "must load assets plugin with precompiled option before loading recheck_precompiled_assets plugin"
53
53
  end
54
+ # Set mutex before overriding a method that uses it
55
+ app::RodaRequest.instance_variable_set(:@_precompiled_assets_mutex, Mutex.new)
54
56
  end
55
57
 
56
58
  def self.configure(app)
57
- precompiled_file = app.assets_opts[:precompiled]
59
+ precompiled_file = app.assets_config.precompiled
58
60
  prev_mtime = ::File.mtime(precompiled_file)
61
+
62
+ request_class = app::RodaRequest
63
+ mutex = request_class.instance_variable_get(:@_precompiled_assets_mutex)
64
+ # Unset the assets matchers, in case a frozen value is already set
65
+ request_class.instance_variable_set(:@assets_matchers, nil)
66
+ # Ensure the assets matchers is set and an unfrozen array, and get the underlying value
67
+ request_class.assets_matchers
68
+ assets_matchers = request_class.instance_variable_get(:@assets_matchers)
69
+
59
70
  app.instance_exec do
60
- opts[:assets] = assets_opts.merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
71
+ _update_compiled_assets(_compiled_assets_initial_hash.replace(assets_config.compiled))
61
72
 
62
73
  define_singleton_method(:recheck_precompiled_assets) do
63
74
  new_mtime = ::File.mtime(precompiled_file)
64
75
  if new_mtime != prev_mtime
65
76
  prev_mtime = new_mtime
66
- assets_opts[:compiled].replace(_precompiled_asset_metadata(precompiled_file))
77
+ assets_config.compiled.replace(_precompiled_asset_metadata(precompiled_file))
67
78
 
68
- # Unset the cached asset matchers, so new ones will be generated.
69
- # This is needed in case the new precompiled metadata uses
70
- # different files.
71
- app::RodaRequest.instance_variable_set(:@assets_matchers, nil)
79
+ # Replace the asset matchers, in case the new precompiled metadata uses different files.
80
+ new_assets_matchers = request_class.send(:_build_assets_matchers)
81
+ mutex.synchronize{assets_matchers.replace(new_assets_matchers)}
72
82
  end
73
83
  end
74
84
  singleton_class.send(:alias_method, :recheck_precompiled_assets, :recheck_precompiled_assets)
@@ -91,8 +101,20 @@ class Roda
91
101
  end
92
102
 
93
103
  module RequestClassMethods
104
+ # Synchronize access and return a dup, so that replacements of the
105
+ # assets matchers at runtime doesn't cause thread safety issues.
106
+ def assets_matchers
107
+ @_precompiled_assets_mutex.synchronize{super.dup}
108
+ end
109
+
94
110
  private
95
111
 
112
+ # Use a unfrozen array for the assets matchers, so they can be replaced
113
+ # at runtime.
114
+ def _build_assets_matchers
115
+ super.dup
116
+ end
117
+
96
118
  # Use a regexp that matches any digest. When the precompiled asset metadata
97
119
  # file is updated, this allows requests for a previous precompiled asset to
98
120
  # still work.
@@ -583,6 +583,9 @@ class Roda
583
583
  opts = subclass.opts[:render] = subclass.render_opts.dup
584
584
  if COMPILED_METHOD_SUPPORT
585
585
  opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
586
+ compiled_templates_module = Module.new
587
+ subclass.send(:include, compiled_templates_module)
588
+ subclass.const_set(:RodaCompiledTemplates, compiled_templates_module)
586
589
  end
587
590
  opts[:cache] = opts[:cache].dup
588
591
  opts.freeze
@@ -76,7 +76,7 @@ class Roda
76
76
 
77
77
  plugin_locals = render_locals
78
78
  if locals = opts[:locals]
79
- plugin_locals = Hash[plugin_locals].merge!(locals)
79
+ plugin_locals = plugin_locals.merge(locals)
80
80
  end
81
81
  opts[:locals] = plugin_locals
82
82
  opts