roda 3.80.0 → 3.82.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0cebe935d536e1f903b212075108ba9cbcade4aa2e8d2abf1c5e0d6e6f539ce8
4
- data.tar.gz: 9b0c576aaa36c5a05596c1bc1bec23d3ab0f37c56dc72342bfa962b10442928f
3
+ metadata.gz: bdf5797c96af1d28dc4d13f0c95ac0468d60eb30b6a128b2cee2c26a08eb0a7f
4
+ data.tar.gz: d687b3cd03657bdcfda6f87a7a7a2a3b55e9a0c4f856cabd3d080865cc7d9ad2
5
5
  SHA512:
6
- metadata.gz: 0e83d0fe8e1f70bb196ea5f285f7a00590ab1669e4b1d686f859d1e5c65a8e760d1320b345306740d00c7d063f0f6bada1af88433b3b04c6b429f439bb7e2521
7
- data.tar.gz: e9b0ffd5b5fb7e976a971f11fbed4f0beb35868dcb7aa70c41c005046a9053cb6c62caf29f669c02cd53e67cd5db51a28076f058824c90a1c1b917a9b7f0f4fb
6
+ metadata.gz: e518b9638e98713ed54fd4c609069edf81a81987adf53ce2b7a09fa7110244f67b109cbe91c9c03d713f824900649d1c4b2ad435823852dcdfd01094f506afae
7
+ data.tar.gz: '00877a1338b70a5b96dab7fb946e21c7c916c776666286f80251b362e29ca6e040ffeb9b7a7258cf3af4c9246de5a6c35c5332385bd3cb926505776102c9112c'
data/CHANGELOG CHANGED
@@ -1,3 +1,19 @@
1
+ = 3.82.0 (2024-07-12)
2
+
3
+ * Add :encodings option to public plugin to support configurable encoding order (jeremyevans)
4
+
5
+ * Add :zstd option to public plugin to supplement it to serve zstd-compressed files with .zst extension (jeremyevans)
6
+
7
+ * Make capture_erb plugin call integrate better with erubi/capture_block (jeremyevans)
8
+
9
+ = 3.81.0 (2024-06-12)
10
+
11
+ * Make assets plugin :early_hints option follow Rack 3 SPEC if using Rack 3 (jeremyevans)
12
+
13
+ * Correctly parse Ruby 3.4 backtraces in exception_page plugin (jeremyevans)
14
+
15
+ * Support :until and :seconds option in hmac_paths plugin, for paths valid only until a specific time (jeremyevans)
16
+
1
17
  = 3.80.0 (2024-05-10)
2
18
 
3
19
  * Support :namespace option in hmac_paths plugin, allowing for easy per-user/per-group HMAC paths (jeremyevans)
@@ -0,0 +1,24 @@
1
+ = New Features
2
+
3
+ * The hmac_paths plugin now supports :until and :seconds options for
4
+ hmac_path, to create a path that is only valid for a specific amount of
5
+ time. :until sets a specific time that the path will be valid until,
6
+ and :seconds makes the path only valid for the given number of seconds.
7
+
8
+ hmac_path('/widget/1', until: Time.utc(2100))
9
+ # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
10
+
11
+ Requests for the path after the given time will not be matched by
12
+ r.hmac_path.
13
+
14
+ = Other Improvements
15
+
16
+ * The early_hints plugin now correctly follows the Rack 3 SPEC when
17
+ using Rack 3. This was not caught previously because Rack only
18
+ added official support for early_hints in the last month.
19
+
20
+ * Ruby 3.4 backtraces are now parsed correctly in the exception_page
21
+ plugin.
22
+
23
+ * Some plugins that accept a block no longer issue an unused block
24
+ warning on Ruby 3.4.
@@ -0,0 +1,43 @@
1
+ = New Features
2
+
3
+ * A :zstd option has been added to the public and multi_public
4
+ plugins to support serving zstd-compressed files with a .zst
5
+ extension. This option is similar to the existing :gzip and
6
+ :brotli plugin options. Chrome started supporting zstd encoding
7
+ in March.
8
+
9
+ * An :encodings option has been added to the public and multi_public
10
+ plugins, for more control over how encodings are handled. This
11
+ allows for changing the order in which encodings are attempted, the
12
+ use of custom encodings, and the use of different file extensions
13
+ for encodings. Example:
14
+
15
+ plugin :public, encodings: {'zstd'=>'.zst', 'deflate'=>'.deflate'}
16
+
17
+ If the :encodings option is not provided, the :zstd, :brotli, and
18
+ :gzip options are used to build an equivalent :encodings option.
19
+
20
+ = Other Improvements
21
+
22
+ * The capture_erb plugin now integrates better when using
23
+ erubi/capture_block for <%= method do %> support in ERB templates,
24
+ using the native capture method provided by the buffer object.
25
+
26
+ * Encoding handling has been more optimized in the public plugin.
27
+ Regexps for the encodings are precomputed, avoiding a regexp
28
+ allocation per request per encoding attempted. On Ruby 2.4+
29
+ Regexp#match? is used for better performance. If the
30
+ Accept-Encoding header is not present, no encoding matching
31
+ is attemped.
32
+
33
+ = Backwards Compatibility
34
+
35
+ * The private public_serve_compressed request method in the public
36
+ plugin now assumes it is called after the encoding is already
37
+ valid. If you are calling this method in your own code, you now
38
+ need to perform checks to make sure the client can accept the
39
+ encoding before calling this method.
40
+
41
+ * The :public_gzip and :public_brotli application options are no
42
+ longer set by the public plugin. The :public_encodings option
43
+ is now set.
@@ -736,7 +736,9 @@ class Roda
736
736
  paths = assets_paths(type)
737
737
  if o[:early_hints]
738
738
  early_hint_as = ltype == :js ? 'script' : 'style'
739
- send_early_hints('Link'=>paths.map{|p| "<#{p}>; rel=preload; as=#{early_hint_as}"}.join("\n"))
739
+ early_hints = paths.map{|p| "<#{p}>; rel=preload; as=#{early_hint_as}"}
740
+ early_hints = early_hints.join("\n") if Rack.release < '3'
741
+ send_early_hints(RodaResponseHeaders::LINK=>early_hints)
740
742
  end
741
743
  paths.map{|p| "#{tag_start}#{h(p)}#{tag_end}"}.join("\n")
742
744
  end
@@ -15,6 +15,11 @@ class Roda
15
15
  # inside templates. It can be combined with the inject_erb plugin
16
16
  # to wrap template blocks with arbitrary output and then inject the
17
17
  # wrapped output into the template.
18
+ #
19
+ # If the output buffer object responds to +capture+ (e.g. when
20
+ # +erubi/capture_block+ is being used as the template engine),
21
+ # this will call +capture+ on the output buffer object, instead
22
+ # of setting the output buffer object temporarily to a new object.
18
23
  module CaptureERB
19
24
  def self.load_dependencies(app)
20
25
  app.plugin :render
@@ -25,13 +30,20 @@ class Roda
25
30
  # with an empty string, and then yield to the block.
26
31
  # Return the value of the block, converted to a string.
27
32
  # Restore the previous ERB output buffer before returning.
28
- def capture_erb
33
+ def capture_erb(&block)
29
34
  outvar = render_opts[:template_opts][:outvar]
30
35
  buf_was = instance_variable_get(outvar)
31
- instance_variable_set(outvar, String.new)
32
- yield.to_s
33
- ensure
34
- instance_variable_set(outvar, buf_was) if outvar && buf_was
36
+
37
+ if buf_was.respond_to?(:capture)
38
+ buf_was.capture(&block)
39
+ else
40
+ begin
41
+ instance_variable_set(outvar, String.new)
42
+ yield.to_s
43
+ ensure
44
+ instance_variable_set(outvar, buf_was) if outvar && buf_was
45
+ end
46
+ end
35
47
  end
36
48
  end
37
49
  end
@@ -245,7 +245,7 @@ END
245
245
 
246
246
  frames = exception.backtrace.map.with_index do |line, i|
247
247
  frame = {:id=>i}
248
- if line =~ /\A(.*?):(\d+)(?::in `(.*)')?\Z/
248
+ if line =~ /\A(.*?):(\d+)(?::in [`'](.*)')?\Z/
249
249
  filename = frame[:filename] = $1
250
250
  lineno = frame[:lineno] = $2.to_i
251
251
  frame[:function] = $3
@@ -21,7 +21,7 @@ class Roda
21
21
  # !request.path.start_with?('/admin/')
22
22
  # end
23
23
  module FilterCommonLogger
24
- def self.load_dependencies(app)
24
+ def self.load_dependencies(app, &_)
25
25
  app.plugin :common_logger
26
26
  end
27
27
 
@@ -112,6 +112,16 @@ class Roda
112
112
  # this for POST requests (or other HTTP verbs that can have request bodies), use +r.GET+
113
113
  # instead of +r.params+ to specifically check query string parameters.
114
114
  #
115
+ # The generated paths can be timestamped, so that they are only valid until a given time
116
+ # or for a given number of seconds after they are generated, using the :until or :seconds
117
+ # options:
118
+ #
119
+ # hmac_path('/widget/1', until: Time.utc(2100))
120
+ # # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
121
+ #
122
+ # hmac_path('/widget/1', seconds: Time.utc(2100).to_i - Time.now.to_i)
123
+ # # => "/dc8b6e56e4cbe7815df7880d42f0e02956b2e4c49881b6060ceb0e49745a540d/t/4102444800/widget/1"
124
+ #
115
125
  # The :namespace option, if provided, should be a string, and it modifies the generated HMACs
116
126
  # to only match those in the same namespace. This can be used to provide different paths to
117
127
  # different users or groups of users.
@@ -190,6 +200,11 @@ class Roda
190
200
  # r.hmac_path('/1', params: {k: 2})
191
201
  # HMAC_hex(HMAC_hex(secret, ''), '/p/1?k=2')
192
202
  #
203
+ # The +:until+ and +:seconds+ option include the timestamp in the HMAC:
204
+ #
205
+ # r.hmac_path('/1', until: Time.utc(2100))
206
+ # HMAC_hex(HMAC_hex(secret, ''), '/t/4102444800/1')
207
+ #
193
208
  # If a +:namespace+ option is provided, the original secret used before the +:root+ option is
194
209
  # an HMAC of the +:secret+ plugin option and the given namespace.
195
210
  #
@@ -232,6 +247,8 @@ class Roda
232
247
  # the already matched path of the routing tree using r.hmac_path. Defaults
233
248
  # to the empty string, which will returns paths valid for r.hmac_path at
234
249
  # the top level of the routing tree.
250
+ # :seconds :: Make the given path valid for the given integer number of seconds.
251
+ # :until :: Make the given path valid until the given Time.
235
252
  def hmac_path(path, opts=OPTS)
236
253
  unless path.is_a?(String) && path.getbyte(0) == 47
237
254
  raise RodaError, "path must be a string starting with /"
@@ -242,6 +259,12 @@ class Roda
242
259
  raise RodaError, "root must be empty string or string starting with /"
243
260
  end
244
261
 
262
+ if valid_until = opts[:until]
263
+ valid_until = valid_until.to_i
264
+ elsif seconds = opts[:seconds]
265
+ valid_until = Time.now.to_i + seconds
266
+ end
267
+
245
268
  flags = String.new
246
269
  path = path.dup
247
270
 
@@ -258,6 +281,11 @@ class Roda
258
281
  flags << 'n'
259
282
  end
260
283
 
284
+ if valid_until
285
+ flags << 't'
286
+ path = "/#{valid_until}#{path}"
287
+ end
288
+
261
289
  flags << '0' if flags.empty?
262
290
 
263
291
  hmac_path = if method
@@ -335,7 +363,19 @@ class Roda
335
363
  end
336
364
 
337
365
  if hmac_path_valid?(mpath, rpath, submitted_hmac, opts)
338
- always(&block)
366
+ if flags.include?('t')
367
+ on Integer do |int|
368
+ if int >= Time.now.to_i
369
+ always(&block)
370
+ else
371
+ # Return from method without matching
372
+ @remaining_path = orig_path
373
+ return
374
+ end
375
+ end
376
+ else
377
+ always(&block)
378
+ end
339
379
  end
340
380
  end
341
381
 
@@ -53,13 +53,10 @@ class Roda
53
53
 
54
54
  class Params < Rack::QueryParser::Params
55
55
  if Rack.release >= '3'
56
- # rack main branch compatibility
57
- # :nocov:
58
56
  if Params < Hash
59
57
  def initialize
60
58
  super(&INDIFFERENT_PROC)
61
59
  end
62
- # :nocov:
63
60
  else
64
61
  def initialize
65
62
  @size = 0
@@ -31,7 +31,7 @@ class Roda
31
31
  # still exists mainly for backward compatibility.
32
32
  module NotFound
33
33
  # Require the status_handler plugin
34
- def self.load_dependencies(app)
34
+ def self.load_dependencies(app, &_)
35
35
  app.plugin :status_handler
36
36
  end
37
37
 
@@ -3,7 +3,7 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The response_headers_plain_hash plugin will change Roda to
6
+ # The plain_hash_response_headers plugin will change Roda to
7
7
  # use a plain hash for response headers. This is Roda's
8
8
  # default behavior on Rack 2, but on Rack 3+, Roda defaults
9
9
  # to using Rack::Headers for response headers for backwards
@@ -44,15 +44,30 @@ class Roda
44
44
  SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
45
45
  PARSER = URI::DEFAULT_PARSER
46
46
  RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
47
+ ENCODING_MAP = {:zstd=>'zstd', :brotli=>'br', :gzip=>'gzip'}.freeze
48
+ ENCODING_EXTENSIONS = {'br'=>'.br', 'gzip'=>'.gz', 'zstd'=>'.zst'}.freeze
49
+
50
+ # :nocov:
51
+ MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
52
+ # :nocov:
47
53
 
48
54
  # Use options given to setup a Rack::File instance for serving files. Options:
55
+ # :brotli :: Whether to serve already brotli-compressed files with a .br extension
56
+ # for clients supporting "br" transfer encoding.
49
57
  # :default_mime :: The default mime type to use if the mime type is not recognized.
58
+ # :encodings :: An enumerable of pairs to handle accepted encodings. The first
59
+ # element of the pair is the accepted encoding name (e.g. 'gzip'),
60
+ # and the second element of the pair is the file extension (e.g.
61
+ # '.gz'). This allows configuration of the order in which encodings
62
+ # are tried, to prefer brotli to zstd for example, or to support
63
+ # encodings other than zstd, brotli, and gzip. This takes
64
+ # precedence over the :brotli, :gzip, and :zstd options if given.
50
65
  # :gzip :: Whether to serve already gzipped files with a .gz extension for clients
51
- # supporting gzipped transfer encoding.
52
- # :brotli :: Whether to serve already brotli-compressed files with a .br extension
53
- # for clients supporting brotli transfer encoding.
66
+ # supporting "gzip" transfer encoding.
54
67
  # :headers :: A hash of headers to use for statically served files
55
68
  # :root :: Use this option for the root of the public directory (default: "public")
69
+ # :zstd :: Whether to serve already zstd-compressed files with a .zst extension
70
+ # for clients supporting "zstd" transfer encoding.
56
71
  def self.configure(app, opts={})
57
72
  if opts[:root]
58
73
  app.opts[:public_root] = app.expand_path(opts[:root])
@@ -60,8 +75,18 @@ class Roda
60
75
  app.opts[:public_root] = app.expand_path("public")
61
76
  end
62
77
  app.opts[:public_server] = RACK_FILES.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
63
- app.opts[:public_gzip] = opts[:gzip]
64
- app.opts[:public_brotli] = opts[:brotli]
78
+
79
+ unless encodings = opts[:encodings]
80
+ if ENCODING_MAP.any?{|k,| opts.has_key?(k)}
81
+ encodings = ENCODING_MAP.map{|k, v| [v, ENCODING_EXTENSIONS[v]] if opts[k]}.compact
82
+ end
83
+ end
84
+ encodings = (encodings || app.opts[:public_encodings] || EMPTY_ARRAY).map(&:dup).freeze
85
+ encodings.each do |a|
86
+ a << /\b#{a[0]}\b/
87
+ end
88
+ encodings.each(&:freeze)
89
+ app.opts[:public_encodings] = encodings
65
90
  end
66
91
 
67
92
  module RequestMethods
@@ -102,8 +127,13 @@ class Roda
102
127
  roda_opts = roda_class.opts
103
128
  path = ::File.join(server.root, *public_path_segments(path))
104
129
 
105
- public_serve_compressed(server, path, '.br', 'br') if roda_opts[:public_brotli]
106
- public_serve_compressed(server, path, '.gz', 'gzip') if roda_opts[:public_gzip]
130
+ if accept_encoding = env['HTTP_ACCEPT_ENCODING']
131
+ roda_opts[:public_encodings].each do |enc, ext, regexp|
132
+ if regexp.send(MATCH_METHOD, accept_encoding)
133
+ public_serve_compressed(server, path, ext, enc)
134
+ end
135
+ end
136
+ end
107
137
 
108
138
  if public_file_readable?(path)
109
139
  s, h, b = public_serve(server, path)
@@ -113,22 +143,22 @@ class Roda
113
143
  end
114
144
  end
115
145
 
146
+ # Serve the compressed file if it exists. This should only
147
+ # be called if the client will accept the related encoding.
116
148
  def public_serve_compressed(server, path, suffix, encoding)
117
- if env['HTTP_ACCEPT_ENCODING'] =~ /\b#{encoding}\b/
118
- compressed_path = path + suffix
149
+ compressed_path = path + suffix
119
150
 
120
- if public_file_readable?(compressed_path)
121
- s, h, b = public_serve(server, compressed_path)
122
- headers = response.headers
123
- headers.replace(h)
124
-
125
- unless s == 304
126
- headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
127
- headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding
128
- end
151
+ if public_file_readable?(compressed_path)
152
+ s, h, b = public_serve(server, compressed_path)
153
+ headers = response.headers
154
+ headers.replace(h)
129
155
 
130
- halt [s, headers, b]
156
+ unless s == 304
157
+ headers[RodaResponseHeaders::CONTENT_TYPE] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
158
+ headers[RodaResponseHeaders::CONTENT_ENCODING] = encoding
131
159
  end
160
+
161
+ halt [s, headers, b]
132
162
  end
133
163
  end
134
164
 
@@ -148,6 +148,19 @@ class Roda
148
148
  # inject the content into the output. To get similar behavior with Roda, you have
149
149
  # a few different options you can use.
150
150
  #
151
+ # == Use Erubi::CaptureBlockEngine
152
+ #
153
+ # Roda defaults to using Erubi for erb template rendering. Erubi 1.13.0+ includes
154
+ # support for an erb variant that supports blocks in <tt><%=</tt> and <tt><%==</tt>
155
+ # tags. To use it:
156
+ #
157
+ # require 'erubi/capture_block'
158
+ # plugin :render, template_opts: {engine_class: Erubi::CaptureBlockEngine}
159
+ #
160
+ # See the Erubi documentation for how to capture data inside the block. Make sure
161
+ # the method call (+some_method+ in the example) returns the output you want added
162
+ # to the rendered body.
163
+ #
151
164
  # == Directly Inject Template Output
152
165
  #
153
166
  # You can switch from a <tt><%=</tt> tag to using a <tt><%</tt> tag:
@@ -172,7 +172,7 @@ class Roda
172
172
  # a valid CSRF token was not provided.
173
173
  class InvalidToken < RodaError; end
174
174
 
175
- def self.load_dependencies(app, opts=OPTS)
175
+ def self.load_dependencies(app, opts=OPTS, &_)
176
176
  app.plugin :_base64
177
177
  end
178
178
 
@@ -476,7 +476,7 @@ class Roda
476
476
  serialized_data << json_data
477
477
 
478
478
  cipher_secret = opts[:cipher_secret]
479
- if per_cookie_secret = opts[:per_cookie_cipher_secret]
479
+ if opts[:per_cookie_cipher_secret]
480
480
  version = "\1"
481
481
  per_cookie_secret_base = SecureRandom.random_bytes(32)
482
482
  cipher_secret = OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, cipher_secret, per_cookie_secret_base)
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 80
7
+ RodaMinorVersion = 82
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.80.0
4
+ version: 3.82.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-10 00:00:00.000000000 Z
11
+ date: 2024-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -122,20 +122,6 @@ dependencies:
122
122
  - - ">="
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0'
125
- - !ruby/object:Gem::Dependency
126
- name: sassc
127
- requirement: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- type: :development
133
- prerelease: false
134
- version_requirements: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
125
  - !ruby/object:Gem::Dependency
140
126
  name: json
141
127
  requirement: !ruby/object:Gem::Requirement
@@ -255,6 +241,8 @@ extra_rdoc_files:
255
241
  - doc/release_notes/3.79.0.txt
256
242
  - doc/release_notes/3.8.0.txt
257
243
  - doc/release_notes/3.80.0.txt
244
+ - doc/release_notes/3.81.0.txt
245
+ - doc/release_notes/3.82.0.txt
258
246
  - doc/release_notes/3.9.0.txt
259
247
  files:
260
248
  - CHANGELOG
@@ -342,6 +330,8 @@ files:
342
330
  - doc/release_notes/3.79.0.txt
343
331
  - doc/release_notes/3.8.0.txt
344
332
  - doc/release_notes/3.80.0.txt
333
+ - doc/release_notes/3.81.0.txt
334
+ - doc/release_notes/3.82.0.txt
345
335
  - doc/release_notes/3.9.0.txt
346
336
  - lib/roda.rb
347
337
  - lib/roda/cache.rb
@@ -507,7 +497,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
507
497
  - !ruby/object:Gem::Version
508
498
  version: '0'
509
499
  requirements: []
510
- rubygems_version: 3.5.9
500
+ rubygems_version: 3.5.11
511
501
  signing_key:
512
502
  specification_version: 4
513
503
  summary: Routing tree web toolkit