roda 3.80.0 → 3.82.0

Sign up to get free protection for your applications and to get access to all the features.
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