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
@@ -11,11 +11,11 @@ class Roda
11
11
  # For a list of compatible browsers, see
12
12
  # http://caniuse.com/#search=link-rel-preload
13
13
  #
14
- # The plugin provides two functions - preload_assets_link_header and
15
- # preload_assets_link_tags. The resulting preloading should be
16
- # identical, it is up to you which system you prefer.
14
+ # The plugin provides two functions: +preload_assets_link_header+ and
15
+ # +preload_assets_link_tags+. The resulting preloading should be
16
+ # identical, it is up to you which approach you prefer.
17
17
  #
18
- # preload_assets_link_header returns a string suitable for populating
18
+ # +preload_assets_link_header+ returns a string suitable for populating
19
19
  # the response Link header:
20
20
  #
21
21
  # response.headers['Link'] = preload_assets_link_header(:css)
@@ -43,7 +43,6 @@ class Roda
43
43
  #
44
44
  # # generate Link header for css assets and js asset groups app and bar
45
45
  # preload_assets_link_header(:css, [:js, :app])
46
- #
47
46
  module AssetsPreloading
48
47
  TYPE_AS = {
49
48
  :css => 'style'.freeze,
@@ -59,27 +58,44 @@ class Roda
59
58
  # Return a string of <link> tags for the given asset
60
59
  # types/groups.
61
60
  def preload_assets_link_tags(*args)
62
- _preload_assets_array(args).map{|path, as| "<link href=\"#{h(path)}\" rel=\"preload\" as=\"#{as}\">"}.join("\n")
61
+ _each_preload_asset(args, "\n") do |path, as|
62
+ "<link href=\"#{h(path)}\" rel=\"preload\" as=\"#{as}\">"
63
+ end
63
64
  end
64
65
 
65
66
  # Return a string suitable for a Link header for the
66
67
  # given asset types/groups.
67
68
  def preload_assets_link_header(*args)
68
- _preload_assets_array(args).map{|path, as| "<#{path}>;rel=preload;as=#{as}"}.join(",")
69
+ _each_preload_asset(args, ",") do |path, as|
70
+ "<#{path}>;rel=preload;as=#{as}"
71
+ end
69
72
  end
70
73
 
71
74
  private
72
75
 
73
76
  # Return an array of paths/as pairs for the given asset
74
77
  # types and/or groups.
75
- def _preload_assets_array(assets)
76
- assets.flat_map do |type|
78
+ def _each_preload_asset(assets, sep)
79
+ output = String.new
80
+ append_sep = false
81
+
82
+ assets.each do |type|
83
+ if append_sep
84
+ output << sep
85
+ else
86
+ append_sep = sep
87
+ end
88
+
77
89
  paths = assets_paths(type)
78
90
  type = type[0] if type.is_a?(Array)
79
91
  as = TYPE_AS[type]
80
92
 
81
- paths.map{|path| [path, as]}
93
+ paths.each do |path|
94
+ output << yield(path, as)
95
+ end
82
96
  end
97
+
98
+ output
83
99
  end
84
100
  end
85
101
  end
@@ -16,8 +16,8 @@ class Roda
16
16
  # This is because the <tt>'a'</tt> entry in the array matches, which
17
17
  # makes the array match. However, the next matcher is the
18
18
  # terminal matcher (since +r.is+ was used), and since the
19
- # path is not terminal as it still contains +/b+ after
20
- # matching <tt>'a'</tt>.
19
+ # remaining path is not empty as it still contains +/b+ after
20
+ # matching <tt>'a'</tt>, matching fails.
21
21
  #
22
22
  # With the backtracking_array plugin, when the terminal matcher
23
23
  # fails, matching will go on to the next entry in the array,
@@ -17,9 +17,9 @@ class Roda
17
17
  # end
18
18
  # end
19
19
  #
20
- # The locals you specify in the set_layout_locals and set_view_locals methods
20
+ # The locals you specify in the +set_layout_locals+ and +set_view_locals+ methods
21
21
  # have higher precedence than the render_locals plugin options, but lower precedence
22
- # than options you directly pass to the view/render methods.
22
+ # than options you directly pass to the +view+/+render+ methods.
23
23
  module BranchLocals
24
24
  SCOPE_INSTANCE_VARIABLES = [:@_layout_locals, :@_view_locals].freeze
25
25
 
@@ -54,7 +54,7 @@ class Roda
54
54
  def render_locals
55
55
  locals = super
56
56
  if @_view_locals
57
- locals = Hash[locals].merge!(@_view_locals)
57
+ locals = locals.merge(@_view_locals)
58
58
  end
59
59
  locals
60
60
  end
@@ -63,7 +63,7 @@ class Roda
63
63
  def layout_locals
64
64
  locals = super
65
65
  if @_layout_locals
66
- locals = Hash[locals].merge!(@_layout_locals)
66
+ locals = locals.merge(@_layout_locals)
67
67
  end
68
68
  locals
69
69
  end
@@ -3,7 +3,7 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The break plugin supports calling break inside a match block, to
6
+ # The break plugin supports calling +break+ inside a match block, to
7
7
  # return from the block and continue in the routing tree, restoring
8
8
  # the remaining path so that future matchers operating on the path
9
9
  # operate as expected.
@@ -28,7 +28,7 @@ class Roda
28
28
  module RequestMethods
29
29
  private
30
30
 
31
- # Handle passing inside the match block.
31
+ # Handle break inside match blocks, restoring remaining path.
32
32
  def always
33
33
  rp = @remaining_path
34
34
  super
@@ -100,33 +100,31 @@ class Roda
100
100
  end
101
101
  end
102
102
 
103
- # Set the response entity tag using the ETag header.
103
+ # Set the response entity tag using the +ETag+ header.
104
104
  #
105
105
  # The +value+ argument is an identifier that uniquely
106
106
  # identifies the current version of the resource.
107
- # Options:
108
- # :weak :: Use a weak cache validator (a strong cache validator is the default)
109
- # :new_resource :: Whether this etag should match an etag of * (true for POST, false otherwise)
107
+ # Keyword Arguments:
108
+ # +weak+ :: Use a weak cache validator (a strong cache validator is the default)
109
+ # +new_resource+ :: Whether this etag should match an etag of * (true for POST, false otherwise)
110
110
  #
111
- # When the current request includes an If-None-Match header with a
111
+ # When the current request includes an <tt>If-None-Match</tt> header with a
112
112
  # matching etag, immediately returns a response with a 304 or 412 status,
113
113
  # depending on the request method.
114
114
  #
115
- # When the current request includes an If-Match header with an etag that
116
- # doesn't match, or where the :weak option was used, immediately returns a
115
+ # When the current request includes an <tt>If-Match</tt> header with an etag that
116
+ # doesn't match, or where the +weak+ argument was used, immediately returns a
117
117
  # response with a 412 status.
118
- def etag(value, opts=OPTS)
119
- # Before touching this code, please double check RFC 2616 14.24 and 14.26.
120
- weak = opts[:weak]
121
- new_resource = opts.fetch(:new_resource){post?}
122
-
118
+ def etag(value, opts=(no_positional_opts = true; OPTS), weak: opts[:weak], new_resource: opts.fetch(:new_resource){post?}, **kw)
119
+ RodaPlugins.check_keywords(__callee__, no_positional_opts, opts, kw)
120
+
123
121
  res = response
124
122
  e = env
125
123
  res[RodaResponseHeaders::ETAG] = etag = "#{'W/' if weak}\"#{value}\""
126
124
  status = res.status
127
125
 
128
126
  if (!status || (status >= 200 && status < 300) || status == 304)
129
- if etag_matches?(e['HTTP_IF_NONE_MATCH'], etag, new_resource)
127
+ if etag_matches?(e['HTTP_IF_NONE_MATCH'], etag, new_resource, weak: true)
130
128
  res.status = (request_method =~ /\AGET|HEAD|OPTIONS|TRACE\z/i ? 304 : 412)
131
129
  halt
132
130
  end
@@ -143,10 +141,21 @@ class Roda
143
141
  private
144
142
 
145
143
  # Helper method checking if a ETag value list includes the current ETag.
146
- def etag_matches?(list, etag, new_resource)
144
+ # If +weak+ is true, uses the weak comparison that <tt>If-None-Match</tt> requires
145
+ # (RFC 9110 section 13.1.2), which ignores a W/ prefix on both sides.
146
+ def etag_matches?(list, etag, new_resource, weak: false)
147
147
  return unless list
148
148
  return !new_resource if list == '*'
149
- list.to_s.split(/\s*,\s*/).include?(etag)
149
+ list = list.to_s.split(/\s*,\s*/)
150
+ if weak
151
+ etag = etag.sub(/\AW\//, '')
152
+ list.any? do |tag|
153
+ tag.sub!(/\AW\//, '')
154
+ tag == etag
155
+ end
156
+ else
157
+ list.include?(etag)
158
+ end
150
159
  end
151
160
 
152
161
  # Helper method parsing a time value from an HTTP header, returning the
@@ -154,11 +163,12 @@ class Roda
154
163
  def time_from_header(t)
155
164
  Time.httpdate(t).to_i if t
156
165
  rescue ArgumentError
166
+ nil
157
167
  end
158
168
  end
159
169
 
160
170
  module ResponseMethods
161
- # Specify response freshness policy for using the Cache-Control header.
171
+ # Specify response freshness policy for using the <tt>Cache-Control</tt> header.
162
172
  # Options can can any non-value directives (:public, :private, :no_cache,
163
173
  # :no_store, :must_revalidate, :proxy_revalidate), with true as the value.
164
174
  # Options can also contain value directives (:max_age, :s_maxage).
@@ -184,7 +194,7 @@ class Roda
184
194
  # cached for. Also sets the Expires header, useful if you have
185
195
  # HTTP 1.0 clients (Cache-Control is an HTTP 1.1 header).
186
196
  def expires(max_age, opts=OPTS)
187
- cache_control(Hash[opts].merge!(:max_age=>max_age))
197
+ cache_control(opts.merge(:max_age=>max_age))
188
198
  @headers[RodaResponseHeaders::EXPIRES] = (Time.now + max_age).httpdate
189
199
  end
190
200
 
@@ -34,7 +34,7 @@ class Roda
34
34
  #
35
35
  # In this case, the block may return nil, instead of the content of
36
36
  # the template. To handle this case, you can provide the
37
- # <tt>returns: :buffer</tt> option when calling the method (to handle
37
+ # <tt>returns: :buffer</tt> argument when calling the method (to handle
38
38
  # that specific call, or when loading the plugin (to default to that
39
39
  # behavior). Note that if the output buffer object responds to
40
40
  # +capture+ and is not an instance of String, the <tt>returns: :buffer</tt>
@@ -46,9 +46,10 @@ class Roda
46
46
 
47
47
  # Support <tt>returns: :buffer</tt> to default to returning buffer
48
48
  # object.
49
- def self.configure(app, opts=OPTS)
49
+ def self.configure(app, opts=(no_positional_opts = true; OPTS), returns: (opts.has_key?(:returns) ? opts[:returns] : (returns_not_set = true; nil)), **kw)
50
+ RodaPlugins.check_keywords("the capture_erb plugin", no_positional_opts, opts, kw)
50
51
  # RODA4: make returns: :buffer the default behavior
51
- app.opts[:capture_erb_returns] = opts[:returns] if opts.has_key?(:returns)
52
+ app.opts[:capture_erb_returns] = returns unless returns_not_set
52
53
  end
53
54
 
54
55
  module ClassMethods
@@ -61,21 +62,23 @@ class Roda
61
62
  # Return the value of the block, converted to a string.
62
63
  # Restore the previous ERB output buffer before returning.
63
64
  #
64
- # Options:
65
- # :returns :: If set to :buffer, returns the value of the
66
- # template output variable, instead of the return
67
- # value of the block converted to a string. This
68
- # is the default behavior if the template output
69
- # variable supports the +capture+ method and is not
70
- # a String instance.
71
- def capture_erb(opts=OPTS, &block)
65
+ # Keyword arguments:
66
+ # +returns+ :: If set to :buffer, returns the value of the
67
+ # template output variable, instead of the return
68
+ # value of the block converted to a string. This
69
+ # is the default behavior if the template output
70
+ # variable supports the +capture+ method and is not
71
+ # a String instance.
72
+ def capture_erb(opts=(no_positional_opts = true; OPTS), returns: (opts.has_key?(:returns) ? opts[:returns] : (returns_not_set = true; nil)), **kw, &block)
73
+ RodaPlugins.check_keywords(__callee__, no_positional_opts, opts, kw)
74
+
72
75
  outvar = render_opts[:template_opts][:outvar]
73
76
  buf_was = instance_variable_get(outvar)
74
77
 
75
78
  if buf_was.respond_to?(:capture) && !buf_was.instance_of?(String)
76
79
  buf_was.capture(&block)
77
80
  else
78
- returns = opts.fetch(:returns) { self.class.capture_erb_returns }
81
+ returns = self.class.capture_erb_returns if returns_not_set
79
82
 
80
83
  begin
81
84
  instance_variable_set(outvar, String.new)
@@ -23,7 +23,7 @@ class Roda
23
23
  # handling is reduced, since if an error occurs while rendering a template,
24
24
  # a successful response code has already been sent.
25
25
  #
26
- # To use chunked encoding for a response, just call the chunked method
26
+ # To use chunked encoding for a response, call the +chunked+ method
27
27
  # instead of view:
28
28
  #
29
29
  # r.root do
@@ -32,7 +32,7 @@ class Roda
32
32
  #
33
33
  # If you want to execute code after flushing the top part of the layout
34
34
  # template, but before rendering the content template, pass a block to
35
- # chunked:
35
+ # +chunked+:
36
36
  #
37
37
  # r.root do
38
38
  # chunked(:index) do
@@ -40,7 +40,7 @@ class Roda
40
40
  # end
41
41
  # end
42
42
  #
43
- # You can also call delay manually with a block, and the execution of the
43
+ # You can also call +delay+ manually with a block, and the execution of the
44
44
  # block will be delayed until rendering the content template. This is
45
45
  # useful if you want to delay execution for all routes under a branch:
46
46
  #
@@ -56,12 +56,12 @@ class Roda
56
56
  # end
57
57
  # end
58
58
  #
59
- # If you want to chunk all responses, pass the :chunk_by_default option
59
+ # If you want to chunk all responses, pass the +chunk_by_default+ argument
60
60
  # when loading the plugin:
61
61
  #
62
62
  # plugin :chunked, chunk_by_default: true
63
63
  #
64
- # then you can just use the normal view method:
64
+ # then you can just use the normal +view+ method:
65
65
  #
66
66
  # r.root do
67
67
  # view(:index)
@@ -73,17 +73,17 @@ class Roda
73
73
  # enable chunking by default, you need to make sure that none of your
74
74
  # content templates make state changes that affect the layout template.
75
75
  # Additionally, make sure nowhere in your app are you doing any processing
76
- # after the call to view.
76
+ # after the call to +view+.
77
77
  #
78
- # If you use :chunk_by_default, but want to turn off chunking for a view,
79
- # call no_chunk!:
78
+ # If you use +chunk_by_default+, but want to turn off chunking for a view,
79
+ # call +no_chunk!+:
80
80
  #
81
81
  # r.root do
82
82
  # no_chunk!
83
83
  # view(:index)
84
84
  # end
85
85
  #
86
- # Inside your layout or content templates, you can call the flush method
86
+ # Inside your layout or content templates, you can call the +flush+ method
87
87
  # to flush the current result of the template to the user, useful for
88
88
  # streaming large datasets.
89
89
  #
@@ -103,7 +103,7 @@ class Roda
103
103
  # <% flush %><%= render(:subtemplate) %>
104
104
  #
105
105
  # If you want to use chunked encoding when rendering a template, but don't
106
- # want to use a layout, pass the <tt>layout: false</tt> option to chunked.
106
+ # want to use a layout, pass the <tt>layout: false</tt> option to +chunked+.
107
107
  #
108
108
  # r.root do
109
109
  # chunked(:index, layout: false)
@@ -116,8 +116,8 @@ class Roda
116
116
  # env['rack.logger'].error(e)
117
117
  # end
118
118
  #
119
- # It is possible to set @_out_buf to an error notification and call
120
- # flush to output the message to the client inside handle_chunk_error.
119
+ # It is possible to set +@_out_buf+ to an error notification and call
120
+ # +flush+ to output the message to the client inside handle_chunk_error.
121
121
  #
122
122
  # In order for chunking to work, you must make sure that no proxies between
123
123
  # the application and the client buffer responses.
@@ -125,22 +125,22 @@ class Roda
125
125
  # If you are using nginx and have it set to buffer proxy responses by
126
126
  # default, you can turn this off on a per response basis using the
127
127
  # X-Accel-Buffering header. To set this header or similar headers for
128
- # all chunked responses, pass a :headers option when loading the plugin:
128
+ # all chunked responses, pass a +headers+ argument when loading the plugin:
129
129
  #
130
130
  # plugin :chunked, headers: {'X-Accel-Buffering'=>'no'}
131
131
  #
132
- # By default, this plugin does not use Transfer-Encoding: chunked, it only
132
+ # By default, this plugin does not use <tt>Transfer-Encoding: chunked</tt>, it only
133
133
  # returns a body that will stream the response in chunks. If you would like
134
- # to force the use of Transfer-Encoding: chunked, you can use the
135
- # :force_chunked_encoding plugin option. If using the
136
- # :force_chunked_encoding plugin option, chunking will only be used for
137
- # HTTP/1.1 requests since Transfer-Encoding: chunked is only supported
134
+ # to force the use of <tt>Transfer-Encoding: chunked</tt>, you can use the
135
+ # +force_chunked_encoding+ plugin argument. If using the
136
+ # +force_chunked_encoding+ argument, chunking will only be used for
137
+ # HTTP/1.1 requests since <tt>Transfer-Encoding: chunked</tt> is only supported
138
138
  # in HTTP/1.1 (non-HTTP/1.1 requests will have behavior similar to
139
- # calling no_chunk!).
139
+ # calling +no_chunk!+).
140
140
  #
141
141
  # The chunked plugin requires the render plugin, and only works for
142
142
  # template engines that store their template output variable in
143
- # @_out_buf. Also, it only works if the content template is directly
143
+ # +@_out_buf+. Also, it only works if the content template is directly
144
144
  # injected into the layout template without modification.
145
145
  #
146
146
  # If using the chunked plugin with the flash plugin, make sure you
@@ -155,14 +155,16 @@ class Roda
155
155
  app.plugin :render
156
156
  end
157
157
 
158
- # Set plugin specific options. Options:
159
- # :chunk_by_default :: chunk all calls to view by default
160
- # :headers :: Set default additional headers to use when calling view
161
- def self.configure(app, opts=OPTS)
162
- app.opts[:chunk_by_default] = opts[:chunk_by_default]
163
- app.opts[:force_chunked_encoding] = opts[:force_chunked_encoding]
164
- if opts[:headers]
165
- app.opts[:chunk_headers] = (app.chunk_headers || {}).merge(opts[:headers]).freeze
158
+ # Set plugin specific arguments. Keyword arguments:
159
+ # +chunk_by_default+ :: chunk all calls to view by default
160
+ # +force_chunked_encoding+ :: force the use of Transfer-Encoding: chunked for HTTP 1.1 requests
161
+ # +headers+ :: Set default additional headers to use when calling view
162
+ def self.configure(app, opts=(no_positional_opts = true; OPTS), chunk_by_default: opts[:chunk_by_default], force_chunked_encoding: opts[:force_chunked_encoding], headers: opts[:headers], **kw)
163
+ RodaPlugins.check_keywords("the chunked plugin", no_positional_opts, opts, kw)
164
+ app.opts[:chunk_by_default] = chunk_by_default
165
+ app.opts[:force_chunked_encoding] = force_chunked_encoding
166
+ if headers
167
+ app.opts[:chunk_headers] = (app.chunk_headers || {}).merge(headers).freeze
166
168
  end
167
169
  end
168
170
 
@@ -251,7 +253,7 @@ class Roda
251
253
  if opts.empty?
252
254
  opts = template
253
255
  else
254
- opts = Hash[opts].merge!(template)
256
+ opts = opts.merge(template)
255
257
  end
256
258
  end
257
259
 
@@ -295,7 +297,7 @@ class Roda
295
297
  @_out_buf = String.new
296
298
  end
297
299
 
298
- if layout_opts = view_layout_opts(opts)
300
+ if layout_opts = view_layout_opts(opts)
299
301
  @_out_buf = render_template(layout_opts) do
300
302
  flush
301
303
  run_delayed_blocks
@@ -45,13 +45,16 @@ class Roda
45
45
  #
46
46
  # When using the class_level_routing plugin with nested routes, you may also want to use the
47
47
  # delegate plugin to delegate certain instance methods to the request object, so you don't have
48
- # to continually use +request.+ in your routing blocks.
48
+ # to continually use +request.+ in your routing blocks, or use the r plugin so you can use +r+
49
+ # instead of +request+.
49
50
  #
50
51
  # Note that class level routing is implemented via a simple array of routes, so routing performance
51
52
  # will degrade linearly as the number of routes increases. For best performance, you should use
52
53
  # the normal +route+ class method to define your routing tree. This plugin does make it simpler to
53
54
  # add additional routes after the routing tree has already been defined, though.
54
55
  module ClassLevelRouting
56
+ SCOPE_INSTANCE_VARIABLES = [:@_original_remaining_path].freeze
57
+
55
58
  # Initialize the class_routes array when the plugin is loaded. Also, if the application doesn't
56
59
  # currently have a routing block, setup an empty routing block so that things will still work if
57
60
  # a routing block isn't added.
@@ -63,7 +66,7 @@ class Roda
63
66
  RodaPlugins.opt_attr_reader(self, :class_level_routes)
64
67
 
65
68
  # Define routing methods that will store class level routes.
66
- [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |request_meth|
69
+ %i[root on is get post delete head options link patch put trace unlink query].each do |request_meth|
67
70
  define_method(request_meth) do |*args, &block|
68
71
  meth = define_roda_method("class_level_routing_#{request_meth}", :any, &block)
69
72
  class_level_routes << [request_meth, args, meth].freeze
@@ -67,8 +67,8 @@ class Roda
67
67
  # Blocks passed to the class_matchers plugin are evaluated in route
68
68
  # block context.
69
69
  #
70
- # When passing a regexp as a matcher, to class_matcher, you can provide a
71
- # <tt>segment: true</tt> option to speed up the matching on Ruby 2.4+:
70
+ # When passing a regexp as a matcher to class_matcher, you can provide a
71
+ # <tt>segment: true</tt> argument to speed up the matching on Ruby 2.4+:
72
72
  #
73
73
  # symbol_matcher(:employee_id, /E-(\d{6})/, segment: true) do |employee_id|
74
74
  # employee_id.to_i
@@ -103,10 +103,13 @@ class Roda
103
103
  # If providing a registered class or symbol, the block will be called with the
104
104
  # captures returned by the block for the registered class or symbol, or the regexp
105
105
  # captures if no block was registered with the class or symbol. In either case,
106
- # if a block is given, it should return an array with the captures to yield to
107
- # the match block.
108
- def class_matcher(klass, matcher, opts=OPTS, &block)
109
- _symbol_class_matcher(Class, klass, matcher, block, opts) do |meth, (_, regexp, convert_meth, consume_meth)|
106
+ # if a block is given, it should return an object to yield to the match block,
107
+ # or an array to yield multiple arguments to the match block. If the block returns
108
+ # nil or false, it will be treated as not matching.
109
+ def class_matcher(klass, matcher, opts=(no_positional_opts = true; OPTS), segment: opts[:segment], **kw, &block)
110
+ RodaPlugins.check_keywords(__callee__, no_positional_opts, opts, kw)
111
+
112
+ _symbol_class_matcher(Class, klass, matcher, block, segment: segment) do |meth, (_, regexp, convert_meth, consume_meth)|
110
113
  if regexp
111
114
  if consume_meth == :_consume_single_segment
112
115
  define_method(meth){_consume_single_segment(regexp, convert_meth)}
@@ -117,6 +120,8 @@ class Roda
117
120
  define_method(meth){_consume_segment(convert_meth)}
118
121
  end
119
122
  end
123
+
124
+ nil
120
125
  end
121
126
 
122
127
  # Freeze the class_matchers hash when freezing the app.
@@ -19,20 +19,31 @@ class Roda
19
19
  # plugin :common_logger, $stdout
20
20
  # plugin :common_logger, Logger.new('filename')
21
21
  # plugin :common_logger, Logger.new('filename'), method: :debug
22
+ # plugin :common_logger, use_request_ip: true
23
+ #
24
+ # Keyword Arguments:
25
+ # +method+ :: Specifies the method to call on the logger object when logging.
26
+ # +use_request_ip+ :: Use +request.ip+ to get the IP address, instead of the default
27
+ # of checking +HTTP_X_FORWARDED_FOR+ and then +REMOTE_ADDR+. This
28
+ # will become the default behavior starting in Roda 4.
22
29
  module CommonLogger
23
30
  SCOPE_INSTANCE_VARIABLES = [:@_request_timer].freeze
24
31
 
25
32
  MUTATE_LINE = RUBY_VERSION < '2.3' || RUBY_VERSION >= '3'
26
33
  private_constant :MUTATE_LINE
27
34
 
28
- def self.configure(app, logger=nil, opts=OPTS)
35
+ def self.configure(app, logger=nil, opts=(no_positional_opts = true; OPTS), method: opts[:method], use_request_ip: false, **kw)
36
+ RodaPlugins.check_keywords("the common_logger plugin", no_positional_opts, opts, kw)
37
+
29
38
  common_logger = app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr # RODA4: drop setting of option, no longer used
30
- app.opts[:common_logger_meth] = common_logger.method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
39
+ app.opts[:common_logger_meth] = common_logger.method(method || (common_logger.respond_to?(:write) ? :write : :<<))
40
+ app.opts[:common_logger_use_request_ip] = use_request_ip # RODA4: Deefault to true
31
41
  end
32
42
 
33
43
  if RUBY_VERSION >= '2.1'
34
44
  # A timer object for calculating elapsed time.
35
45
  def self.start_timer
46
+ # RODA4.1: Remove
36
47
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
48
  end
38
49
  else
@@ -45,6 +56,7 @@ class Roda
45
56
 
46
57
  module ClassMethods
47
58
  RodaPlugins.opt_attr_reader(self, :common_logger_meth)
59
+ RodaPlugins.opt_attr_reader(self, :common_logger_use_request_ip, name: :common_logger_use_request_ip?)
48
60
  end
49
61
 
50
62
  module InstanceMethods
@@ -62,7 +74,13 @@ class Roda
62
74
 
63
75
  env = @_request.env
64
76
 
65
- line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time} "
77
+ ip = if self.class.common_logger_use_request_ip?
78
+ @_request.ip
79
+ else
80
+ env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"]
81
+ end
82
+
83
+ line = "#{ip || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{status} #{((length = headers[RodaResponseHeaders::CONTENT_LENGTH]) && (length unless length == '0')) || '-'} #{elapsed_time} "
66
84
  if MUTATE_LINE
67
85
  line.gsub!(/[^[:print:]]/){|c| sprintf("\\x%x", c.ord)}
68
86
  # simplecov:disable
@@ -8,15 +8,15 @@ class Roda
8
8
  # use in applications that want to use sessions for some requests,
9
9
  # and want to be sure that sessions are not used for other requests.
10
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:
11
+ # requests with paths starting with +/static+, you could do:
12
12
  #
13
13
  # plugin :conditional_sessions, secret: ENV["SECRET"] do
14
14
  # !path_info.start_with?('/static')
15
15
  # end
16
16
  #
17
- # The the request session, session_created_at, and session_updated_at methods
17
+ # Then the request +session+, +session_created_at+, and +session_updated_at+ methods
18
18
  # raise a RodaError exception when sessions are not allowed. The request
19
- # persist_session and route scope clear_session methods do nothing when
19
+ # +persist_session+ and route scope +clear_session+ methods do nothing when
20
20
  # sessions are not allowed.
21
21
  module ConditionalSessions
22
22
  # Pass all options to the sessions block, and use the block to define
@@ -11,7 +11,7 @@ class Roda
11
11
  # of the normal content pane.
12
12
  #
13
13
  # In the template in which you want to store content, call
14
- # content_for with a block:
14
+ # +content_for+ with a block:
15
15
  #
16
16
  # <% content_for :foo do %>
17
17
  # Some content here.
@@ -27,11 +27,11 @@ class Roda
27
27
  # <% content_for :foo, "Some content" %>
28
28
  #
29
29
  # In the template in which you want to retrieve content,
30
- # call content_for without the block or argument:
30
+ # call +content_for+ without the block or argument:
31
31
  #
32
32
  # <%= content_for :foo %>
33
33
  #
34
- # Note that when storing content by calling content_for
34
+ # Note that when storing content by calling +content_for+
35
35
  # with a block and embedding template code, the return
36
36
  # value of the block is used as the content (after being
37
37
  # converted to a string). This can cause issues in some
@@ -51,7 +51,7 @@ class Roda
51
51
  # If content_for is used multiple times with the same key,
52
52
  # by default, the last call will append previous calls.
53
53
  # If you want to overwrite the previous content, pass the
54
- # <tt>append: false</tt> option when loading the plugin:
54
+ # <tt>append: false</tt> argument when loading the plugin:
55
55
  #
56
56
  # plugin :content_for, append: false
57
57
  module ContentFor
@@ -65,8 +65,9 @@ class Roda
65
65
 
66
66
  # Configure whether to append or overwrite if content_for
67
67
  # is called multiple times with the same key.
68
- def self.configure(app, opts = OPTS)
69
- app.opts[:append_content_for] = opts.fetch(:append, true)
68
+ def self.configure(app, opts=(no_positional_opts = true; OPTS), append: opts.fetch(:append, true), **kw)
69
+ RodaPlugins.check_keywords("the content_for plugin", no_positional_opts, opts, kw)
70
+ app.opts[:append_content_for] = append
70
71
  end
71
72
 
72
73
  module ClassMethods