roda 3.40.0 → 3.44.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: 8e87645db18482f3ad97d106e268d76b2567d074490f37d7e99d52d06e4e5483
4
- data.tar.gz: b84b082c2f27fcbd31254cb80de5e6610b7417e722eaf3dc3d8753a4604a0701
3
+ metadata.gz: b60182ef0483279ac1d1983495ba020c1803d6c1fe0fa84af882142f816f72bc
4
+ data.tar.gz: 20f6053b7c73fed454dc3f39e55a516d7a5461a4005356b5788480b8aab36f14
5
5
  SHA512:
6
- metadata.gz: 3b21d6fa4b33c029a8b23a8ec437e70e2799e3f2bec27239d3cbfc5a1a4bb8cd82fc95309ad27d22243695298535b02687b0948218df1f7a1a0812242079e3b7
7
- data.tar.gz: 3dc9629b16a4d9f496cee61bc40396f6893bfe114666ba7554327cd052946719410e2f4e89a27388cea291fd7b5ba1bcd37c669aa1dfc7b64c48a6622bafb013
6
+ metadata.gz: 89359f0c972c236bee671606fc7254f4999c6fc03f1b0ef8ff7c9691b581b6e796197b9fbee6fd109ae4d1026624f041d2e64c6182dbf8c80818756543923c9d
7
+ data.tar.gz: 2d786d8641f5195a65874f3326abe07057748e4de2df7d0a8e8cd9439b6afceb08e65b6a3733e15b0289e273cc070033c1dc466e1909880410b4d05541fa04ae
data/CHANGELOG CHANGED
@@ -1,3 +1,33 @@
1
+ = 3.44.0 (2021-05-12)
2
+
3
+ * Add optimized_segment_matchers plugin for optimized matchers for a single String class argument (jeremyevans)
4
+
5
+ * Use RFC 5987 UTF-8 and ISO-8859-1 encoded filenames when using send_file and attachment in the sinatra_helpers plugin (jeremyevans)
6
+
7
+ = 3.43.1 (2021-04-13)
8
+
9
+ * [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
10
+
11
+ = 3.43.0 (2021-04-12)
12
+
13
+ * Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
14
+
15
+ = 3.42.0 (2021-03-12)
16
+
17
+ * Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
18
+
19
+ * Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207)
20
+
21
+ * Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206)
22
+
23
+ * Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans)
24
+
25
+ * Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans)
26
+
27
+ = 3.41.0 (2021-02-17)
28
+
29
+ * Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans)
30
+
1
31
  = 3.40.0 (2021-01-14)
2
32
 
3
33
  * Add freeze_template_caches! to the precompile_templates plugin, which ensures all templates are precompiled, and speeds up template access (jeremyevans)
@@ -0,0 +1,9 @@
1
+ = Improvements
2
+
3
+ * The performance of the render plugin's view method when passed the
4
+ :content option and no other options or arguments has been improved
5
+ by about 3x, by calling compiled template methods directly.
6
+
7
+ * The compiled template method for the layout is cleared when the
8
+ render plugin is loaded again, which can fix issues when it is
9
+ loaded with different options that affect the layout.
@@ -0,0 +1,21 @@
1
+ = New Features
2
+
3
+ * A recheck_precompiled_assets plugin has been added, which allows
4
+ for checking for updates to the precompiled asset metadata file,
5
+ and automatically using the updated data.
6
+
7
+ * The common_logger plugin now supports a :method plugin option to
8
+ specify the method to call on the logger.
9
+
10
+ = Other Improvements
11
+
12
+ * Plugins and middleware that use keyword arguments are now supported
13
+ in Ruby 3.
14
+
15
+ * The compile_assets class method in the assets plugin now uses an
16
+ atomic approach to writing the precompiled asset metadata file.
17
+
18
+ * Minor method visibility issues have been fixed. The custom_matchers
19
+ plugin no longer makes the unsupported_matcher request method
20
+ public, and the render plugin no longer makes the _layout_method
21
+ public when the application is frozen.
@@ -0,0 +1,34 @@
1
+ = New Features
2
+
3
+ * A host_authorization plugin has been added to verify the requested
4
+ Host header is authorized. Using it can prevent DNS rebinding
5
+ attacks in cases where the application can receive requests for
6
+ arbitrary hosts.
7
+
8
+ To check for authorized hosts in your routing tree, you call the
9
+ check_host_authorization! method. For example, if you want to
10
+ check for authorized hosts after serving requests for public
11
+ files, you could do:
12
+
13
+ plugin :public
14
+ plugin :host_authorization, 'my-domain-name.example.com'
15
+
16
+ route do |r|
17
+ r.public
18
+ check_host_authorized!
19
+
20
+ # ... rest of routing tree
21
+ end
22
+
23
+ In addition to handling single domain names via a string, you can
24
+ provide an array of domain names, a regexp to match again, or a
25
+ proc.
26
+
27
+ By default, requests using unauthorized hosts receive an empty 403
28
+ response. If you would like to customize the response, you can
29
+ pass a block when loading the plugin:
30
+
31
+ plugin :host_authorization, 'my-domain-name.example.com' do |r|
32
+ response.status = 403
33
+ "Response Body Here"
34
+ end
@@ -0,0 +1,23 @@
1
+ = New Features
2
+
3
+ * An optimized_segment_matchers plugin has been added that offers
4
+ very fast matchers for arbitrary segments (the same segments
5
+ that would be matched by the String class matcher). The
6
+ on_segment method it offers accepts no arguments and yields
7
+ the next segment if there is a segment. The is_segment method
8
+ is similar, but only yields if the next segment is the final
9
+ segment.
10
+
11
+ = Other Improvements
12
+
13
+ * The send_file and attachment methods in the sinatra_helpers plugin
14
+ now support RFC 5987 UTF-8 and ISO-8859-1 encoded filenames,
15
+ allowing modern browsers to save files with encoded chracters. For
16
+ older browsers that do not support RFC 5987, unsupported characters
17
+ in filenames are replaced with dashes. This is considered to be an
18
+ improvement over the previous behavior of using Ruby's inspect
19
+ output for the filename, which could contain backslashes (backslash
20
+ is not an allowed chracter in Windows filenames).
21
+
22
+ * The performance of the String class matcher has been slightly
23
+ improved.
data/lib/roda.rb CHANGED
@@ -292,6 +292,9 @@ class Roda
292
292
  plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
293
293
  @app = nil
294
294
  end
295
+ # :nocov:
296
+ ruby2_keywords(:plugin) if respond_to?(:ruby2_keywords, true)
297
+ # :nocov:
295
298
 
296
299
  # Setup routing tree for the current Roda application, and build the
297
300
  # underlying rack application using the stored middleware. Requires
@@ -327,6 +330,9 @@ class Roda
327
330
  @middleware << [args, block].freeze
328
331
  @app = nil
329
332
  end
333
+ # :nocov:
334
+ ruby2_keywords(:use) if respond_to?(:ruby2_keywords, true)
335
+ # :nocov:
330
336
 
331
337
  private
332
338
 
@@ -376,7 +376,7 @@ class Roda
376
376
 
377
377
  if opts[:precompiled] && !opts[:compiled] && ::File.exist?(opts[:precompiled])
378
378
  require 'json'
379
- opts[:compiled] = (app.opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(opts[:precompiled]))
379
+ opts[:compiled] = app.send(:_precompiled_asset_metadata, opts[:precompiled])
380
380
  end
381
381
 
382
382
  if opts[:early_hints]
@@ -455,7 +455,7 @@ class Roda
455
455
  require 'fileutils'
456
456
 
457
457
  unless assets_opts[:compiled]
458
- opts[:assets] = assets_opts.merge(:compiled => {})
458
+ opts[:assets] = assets_opts.merge(:compiled => _compiled_assets_initial_hash).freeze
459
459
  end
460
460
 
461
461
  if type == nil
@@ -465,10 +465,12 @@ class Roda
465
465
  _compile_assets(type)
466
466
  end
467
467
 
468
- if assets_opts[:precompiled]
468
+ if precompile_file = assets_opts[:precompiled]
469
469
  require 'json'
470
- ::FileUtils.mkdir_p(File.dirname(assets_opts[:precompiled]))
471
- ::File.open(assets_opts[:precompiled], 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
470
+ ::FileUtils.mkdir_p(File.dirname(precompile_file))
471
+ tmp_file = "#{precompile_file}.tmp"
472
+ ::File.open(tmp_file, 'wb'){|f| f.write((opts[:json_serializer] || :to_json.to_proc).call(assets_opts[:compiled]))}
473
+ ::File.rename(tmp_file, precompile_file)
472
474
  end
473
475
 
474
476
  assets_opts[:compiled]
@@ -476,6 +478,11 @@ class Roda
476
478
 
477
479
  private
478
480
 
481
+ # The initial hash to use to store compiled asset metadata.
482
+ def _compiled_assets_initial_hash
483
+ {}
484
+ end
485
+
479
486
  # Internals of compile_assets, handling recursive calls for loading
480
487
  # all asset groups under the given type.
481
488
  def _compile_assets(type)
@@ -493,6 +500,11 @@ class Roda
493
500
  end
494
501
  end
495
502
 
503
+ # The precompiled asset metadata stored in the given file
504
+ def _precompiled_asset_metadata(file)
505
+ (opts[:json_parser] || ::JSON.method(:parse)).call(::File.read(file))
506
+ end
507
+
496
508
  # Compile each array of files for the given type into a single
497
509
  # file. Dirs should be an array of asset group names, if these
498
510
  # are files in an asset group.
@@ -794,23 +806,32 @@ class Roda
794
806
  # handled.
795
807
  def assets_matchers
796
808
  @assets_matchers ||= [:css, :js].map do |t|
797
- [t, assets_regexp(t)].freeze if roda_class.assets_opts[t]
809
+ if regexp = assets_regexp(t)
810
+ [t, regexp].freeze
811
+ end
798
812
  end.compact.freeze
799
813
  end
800
814
 
801
815
  private
802
816
 
817
+ # A string for the asset filename for the asset type, key, and digest.
818
+ def _asset_regexp(type, key, digest)
819
+ "#{key.sub(/\A#{type}/, '')}.#{digest}.#{type}"
820
+ end
821
+
803
822
  # The regexp matcher to use for the given type. This handles any asset groups
804
823
  # for the asset types.
805
824
  def assets_regexp(type)
806
825
  o = roda_class.assets_opts
807
826
  if compiled = o[:compiled]
808
- assets = compiled.select{|k,_| k =~ /\A#{type}/}.map do |k, md|
809
- "#{k.sub(/\A#{type}/, '')}.#{md}.#{type}"
810
- end
827
+ assets = compiled.
828
+ select{|k,_| k =~ /\A#{type}/}.
829
+ map{|k, md| _asset_regexp(type, k, md)}
830
+ return if assets.empty?
811
831
  /#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
812
832
  else
813
- assets = unnest_assets_hash(o[type])
833
+ return unless assets = o[type]
834
+ assets = unnest_assets_hash(assets)
814
835
  ts = o[:timestamp_paths]
815
836
  /#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
816
837
  end
@@ -18,10 +18,11 @@ class Roda
18
18
  # plugin :common_logger
19
19
  # plugin :common_logger, $stdout
20
20
  # plugin :common_logger, Logger.new('filename')
21
+ # plugin :common_logger, Logger.new('filename'), method: :debug
21
22
  module CommonLogger
22
- def self.configure(app, logger=nil)
23
+ def self.configure(app, logger=nil, opts=OPTS)
23
24
  app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr
24
- app.opts[:common_logger_meth] = app.opts[:common_logger].method(logger.respond_to?(:write) ? :write : :<<)
25
+ app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
25
26
  end
26
27
 
27
28
  if RUBY_VERSION >= '2.1'
@@ -68,6 +68,8 @@ class Roda
68
68
  end
69
69
 
70
70
  module RequestMethods
71
+ private
72
+
71
73
  # Try custom matchers before calling super
72
74
  def unsupported_matcher(matcher)
73
75
  roda_class.opts[:custom_matchers].each do |match_class, meth|
@@ -23,30 +23,31 @@ class Roda
23
23
  module DefaultHeaders
24
24
  # Merge the given headers into the existing default headers, if any.
25
25
  def self.configure(app, headers={})
26
- headers = app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
26
+ app.opts[:default_headers] = (app.default_headers || app::RodaResponse::DEFAULT_HEADERS).merge(headers).freeze
27
+ end
28
+
29
+ module ClassMethods
30
+ # The default response headers to use for the current class.
31
+ def default_headers
32
+ opts[:default_headers]
33
+ end
27
34
 
28
- if headers.all?{|k, v| k.is_a?(String) && v.is_a?(String)}
29
- response_class = app::RodaResponse
30
- owner = response_class.instance_method(:set_default_headers).owner
31
- if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
32
- app.opts[:set_default_headers_overridder] = response_class
33
- response_class.class_eval(<<-END, __FILE__, __LINE__+1)
35
+ # Optimize the response class set_default_headers method if it hasn't been
36
+ # overridden and all default headers are strings.
37
+ def freeze
38
+ if (headers = opts[:default_headers]).all?{|k, v| k.is_a?(String) && v.is_a?(String)} &&
39
+ (self::RodaResponse.instance_method(:set_default_headers).owner == Base::ResponseMethods)
40
+ self::RodaResponse.class_eval(<<-END, __FILE__, __LINE__+1)
34
41
  private
35
42
 
36
- alias set_default_headers set_default_headers
37
43
  def set_default_headers
38
44
  h = @headers
39
45
  #{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
40
46
  end
41
47
  END
42
48
  end
43
- end
44
- end
45
49
 
46
- module ClassMethods
47
- # The default response headers to use for the current class.
48
- def default_headers
49
- opts[:default_headers]
50
+ super
50
51
  end
51
52
  end
52
53
 
@@ -0,0 +1,156 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The host_authorization plugin allows configuring an authorized host or
7
+ # an array of authorized hosts. Then in the routing tree, you can check
8
+ # whether the request uses an authorized host via the +check_host_authorized!+
9
+ # method.
10
+ #
11
+ # If the request doesn't match one of the authorized hosts, the
12
+ # request processing stops at that point. Using this plugin can prevent
13
+ # DNS rebinding attacks if the application can receive requests for
14
+ # arbitrary hosts.
15
+ #
16
+ # By default, an empty response using status 403 will be returned for requests
17
+ # with unauthorized hosts.
18
+ #
19
+ # Because +check_host_authorized!+ is an instance method, you can easily choose
20
+ # to only check for authorization in certain routes, or to check it after
21
+ # other processing. For example, you could check for authorized hosts after
22
+ # serving static files, since the serving of static files should not be
23
+ # vulnerable to DNS rebinding attacks.
24
+ #
25
+ # = Usage
26
+ #
27
+ # In your routing tree, call the +check_host_authorized!+ method at the point you
28
+ # want to check for authorized hosts:
29
+ #
30
+ # plugin :host_authorization, 'www.example.com'
31
+ # plugin :public
32
+ #
33
+ # route do |r|
34
+ # r.public
35
+ # check_host_authorized!
36
+ #
37
+ # # ...
38
+ # end
39
+ #
40
+ # = Specifying authorized hosts
41
+ #
42
+ # For applications hosted on a single domain name, you can use a single string:
43
+ #
44
+ # plugin :host_authorization, 'www.example.com'
45
+ #
46
+ # For applications hosted on multiple domain names, you can use an array of strings:
47
+ #
48
+ # plugin :host_authorization, %w'www.example.com www.example2.com'
49
+ #
50
+ # For applications supporting arbitrary subdomains, you can use a regexp. If using
51
+ # a regexp, make sure you use <tt>\A<tt> and <tt>\z</tt> in your regexp, and restrict
52
+ # the allowed characters to the minimum required, otherwise you can potentionally
53
+ # introduce a security issue:
54
+ #
55
+ # plugin :host_authorization, /\A[-0-9a-f]+\.example\.com\z/
56
+ #
57
+ # For applications with more complex requirements, you can use a proc. Similarly
58
+ # to the regexp case, the proc should be aware the host contains user-submitted
59
+ # values, and not assume it is in any particular format:
60
+ #
61
+ # plugin :host_authorization, proc{|host| ExternalService.allowed_host?(host)}
62
+ #
63
+ # If an array of values is passed as the host argument, the host is authorized if
64
+ # it matches any value in the array. All host authorization checks use the
65
+ # <tt>===</tt> method, which is why it works for strings, regexps, and procs.
66
+ # It can also work with arbitrary objects that support <tt>===</tt>.
67
+ #
68
+ # For security reasons, only the +Host+ header is checked by default. If you are
69
+ # sure that your application is being run behind a forwarding proxy that sets the
70
+ # <tt>X-Forwarded-Host</tt> header, you should enable support for checking that
71
+ # header using the +:check_forwarded+ option:
72
+ #
73
+ # plugin :host_authorization, 'www.example.com', check_forwarded: true
74
+ #
75
+ # In this case, the trailing host in the <tt>X-Forwarded-Host</tt> header is checked,
76
+ # which should be the host set by the forwarding proxy closest to the application.
77
+ # In cases where multiple forwarding proxies are used that append to the
78
+ # <tt>X-Forwarded-Host</tt> header, you should not use this plugin.
79
+ #
80
+ # = Customizing behavior
81
+ #
82
+ # By default, an unauthorized host will receive an empty 403 response. You can
83
+ # customize this by passing a block when loading the plugin. For example, for
84
+ # sites using the render plugin, you could return a page that uses your default
85
+ # layout:
86
+ #
87
+ # plugin :render
88
+ # plugin :host_authorization, 'www.example.com' do |r|
89
+ # response.status = 403
90
+ # view(:content=>"<h1>Forbidden</h1>")
91
+ # end
92
+ #
93
+ # The block passed to this plugin is treated as a match block.
94
+ module HostAuthorization
95
+ def self.configure(app, host, opts=OPTS, &block)
96
+ app.opts[:host_authorization_host] = host
97
+ app.opts[:host_authorization_check_forwarded] = opts[:check_forwarded] if opts.key?(:check_forwarded)
98
+
99
+ if block
100
+ app.define_roda_method(:host_authorization_unauthorized, 1, &block)
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+ # Check whether the host is authorized. If not authorized, return a response
106
+ # immediately based on the plugin block.
107
+ def check_host_authorization!
108
+ r = @_request
109
+ return if host_authorized?(_convert_host_for_authorization(r.env["HTTP_HOST"].to_s.dup))
110
+
111
+ if opts[:host_authorization_check_forwarded] && (host = r.env["HTTP_X_FORWARDED_HOST"])
112
+ if i = host.rindex(',')
113
+ host = host[i+1, 10000000].to_s
114
+ end
115
+ host = _convert_host_for_authorization(host.strip)
116
+
117
+ if !host.empty? && host_authorized?(host)
118
+ return
119
+ end
120
+ end
121
+
122
+ r.on do
123
+ host_authorization_unauthorized(r)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ # Remove the port information from the passed string (mutates the passed argument).
130
+ def _convert_host_for_authorization(host)
131
+ host.sub!(/:\d+\z/, "")
132
+ host
133
+ end
134
+
135
+ # Whether the host given is one of the authorized hosts for this application.
136
+ def host_authorized?(host, authorized_host = opts[:host_authorization_host])
137
+ case authorized_host
138
+ when Array
139
+ authorized_host.any?{|auth_host| host_authorized?(host, auth_host)}
140
+ else
141
+ authorized_host === host
142
+ end
143
+ end
144
+
145
+ # Action to take for unauthorized hosts. Sets a 403 status by default.
146
+ def host_authorization_unauthorized(_)
147
+ @_response.status = 403
148
+ nil
149
+ end
150
+ end
151
+ end
152
+
153
+ register_plugin(:host_authorization, HostAuthorization)
154
+ end
155
+ end
156
+
@@ -0,0 +1,53 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The optimized_segment_matchers plugin adds two optimized matcher methods,
7
+ # +r.on_segment+ and +r.is_segment+. +r.on_segment+ is an optimized version of
8
+ # +r.on String+ that accepts no arguments and yields the next segment if there
9
+ # is a segment. +r.is_segment+ is an optimized version of +r.is String+ that accepts
10
+ # no arguments and yields the next segment only if it is the last segment.
11
+ #
12
+ # plugin :optimized_segment_matchers
13
+ #
14
+ # route do |r|
15
+ # r.on_segment do |x|
16
+ # # matches any segment (e.g. /a, /b, but not /)
17
+ # r.is_segment do |y|
18
+ # # matches only if final segment (e.g. /a/b, /b/c, but not /c, /c/d/, /c/d/e)
19
+ # end
20
+ # end
21
+ # end
22
+ module OptimizedSegmentMatchers
23
+ module RequestMethods
24
+ # Optimized version of +r.on String+ that yields the next segment if there
25
+ # is a segment.
26
+ def on_segment
27
+ rp = @remaining_path
28
+ if rp.getbyte(0) == 47
29
+ if last = rp.index('/', 1)
30
+ @remaining_path = rp[last, rp.length]
31
+ always{yield rp[1, last-1]}
32
+ elsif (len = rp.length) > 1
33
+ @remaining_path = ""
34
+ always{yield rp[1, len]}
35
+ end
36
+ end
37
+ end
38
+
39
+ # Optimized version of +r.is String+ that yields the next segment only if it
40
+ # is the final segment.
41
+ def is_segment
42
+ rp = @remaining_path
43
+ if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1
44
+ @remaining_path = ""
45
+ always{yield rp[1, len]}
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ register_plugin(:optimized_segment_matchers, OptimizedSegmentMatchers)
52
+ end
53
+ end
@@ -19,7 +19,8 @@ class Roda
19
19
  # end
20
20
  # end
21
21
  #
22
- # Note that both of these methods only work with plain strings, not
22
+ # If you are using the placeholder_string_matchers plugin, note
23
+ # that both of these methods only work with plain strings, not
23
24
  # with strings with embedded colons for capturing. Matching will work
24
25
  # correctly in such cases, but the captures will not be yielded to the
25
26
  # match blocks.
@@ -0,0 +1,107 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The recheck_precompiled_assets plugin enables checking for the precompiled asset metadata file.
7
+ # You need to have already loaded the assets plugin with the +:precompiled+ option and the file
8
+ # specified by the +:precompiled+ option must already exist in order to use the
9
+ # recheck_precompiled_assets plugin.
10
+ #
11
+ # Any time you want to check whether the precompiled asset metadata file has changed and should be
12
+ # reloaded, you can call the +recheck_precompiled_assets+ class method. This method will check
13
+ # whether the file has changed, and reload it if it has. If you want to check for modifications on
14
+ # every request, you can use +self.class.recheck_precompiled_assets+ inside your route block.
15
+ module RecheckPrecompiledAssets
16
+ # Thread safe wrapper for the compiled asset metadata hash. Does not wrap all
17
+ # hash methods, only a few that are used.
18
+ class CompiledAssetsHash
19
+ include Enumerable
20
+
21
+ def initialize
22
+ @hash = {}
23
+ @mutex = Mutex.new
24
+ end
25
+
26
+ def [](key)
27
+ @mutex.synchronize{@hash[key]}
28
+ end
29
+
30
+ def []=(key, value)
31
+ @mutex.synchronize{@hash[key] = value}
32
+ end
33
+
34
+ def replace(hash)
35
+ hash = hash.instance_variable_get(:@hash) if (CompiledAssetsHash === hash)
36
+ @mutex.synchronize{@hash.replace(hash)}
37
+ self
38
+ end
39
+
40
+ def each(&block)
41
+ @mutex.synchronize{@hash.dup}.each(&block)
42
+ self
43
+ end
44
+
45
+ def to_json(*args)
46
+ @mutex.synchronize{@hash.dup}.to_json(*args)
47
+ end
48
+ end
49
+
50
+ def self.load_dependencies(app)
51
+ unless app.respond_to?(:assets_opts) && app.assets_opts[:precompiled]
52
+ raise RodaError, "must load assets plugin with precompiled option before loading recheck_precompiled_assets plugin"
53
+ end
54
+ end
55
+
56
+ def self.configure(app)
57
+ precompiled_file = app.assets_opts[:precompiled]
58
+ prev_mtime = ::File.mtime(precompiled_file)
59
+ app.instance_exec do
60
+ opts[:assets] = opts[:assets].merge(:compiled=>_compiled_assets_initial_hash.replace(assets_opts[:compiled])).freeze
61
+
62
+ define_singleton_method(:recheck_precompiled_assets) do
63
+ new_mtime = ::File.mtime(precompiled_file)
64
+ if new_mtime != prev_mtime
65
+ prev_mtime = new_mtime
66
+ assets_opts[:compiled].replace(_precompiled_asset_metadata(precompiled_file))
67
+
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)
72
+ end
73
+ end
74
+ singleton_class.send(:alias_method, :recheck_precompiled_assets, :recheck_precompiled_assets)
75
+ end
76
+ end
77
+
78
+ module ClassMethods
79
+ private
80
+
81
+ # Wrap the precompiled asset metadata in a thread-safe hash.
82
+ def _precompiled_asset_metadata(file)
83
+ CompiledAssetsHash.new.replace(super)
84
+ end
85
+
86
+ # Use a thread-safe wrapper of a hash for the :compiled assets option, since
87
+ # the recheck_precompiled_asset_metadata can modify it at runtime.
88
+ def _compiled_assets_initial_hash
89
+ CompiledAssetsHash.new
90
+ end
91
+ end
92
+
93
+ module RequestClassMethods
94
+ private
95
+
96
+ # Use a regexp that matches any digest. When the precompiled asset metadata
97
+ # file is updated, this allows requests for a previous precompiled asset to
98
+ # still work.
99
+ def _asset_regexp(type, key, _)
100
+ /#{Regexp.escape(key.sub(/\A#{type}/, ''))}\.[0-9a-fA-F]+\.#{type}/
101
+ end
102
+ end
103
+ end
104
+
105
+ register_plugin(:recheck_precompiled_assets, RecheckPrecompiledAssets)
106
+ end
107
+ end
@@ -183,6 +183,7 @@ class Roda
183
183
  app.const_set(:RodaCompiledTemplates, compiled_templates_module)
184
184
  end
185
185
  opts[:template_method_cache] = orig_method_cache || (opts[:cache_class] || RodaCache).new
186
+ opts[:template_method_cache][:_roda_layout] = nil if opts[:template_method_cache][:_roda_layout]
186
187
  opts[:cache] = orig_cache || (opts[:cache_class] || RodaCache).new
187
188
 
188
189
  opts[:layout_opts] = (opts[:layout_opts] || {}).dup
@@ -387,6 +388,8 @@ class Roda
387
388
  instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
388
389
  layout_method = opts[:render][:template_method_cache][:_roda_layout]
389
390
  define_method(:_layout_method){layout_method}
391
+ private :_layout_method
392
+ alias_method(:_layout_method, :_layout_method)
390
393
  opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
391
394
  end
392
395
  end
@@ -415,10 +418,8 @@ class Roda
415
418
  # Render the given template. If there is a default layout
416
419
  # for the class, take the result of the template rendering
417
420
  # and render it inside the layout. See Render for details.
418
- def view(template, opts = (optimized_template = _cached_template_method(template); OPTS))
419
- if optimized_template
420
- content = send(optimized_template, OPTS)
421
-
421
+ def view(template, opts = (content = _optimized_view_content(template); OPTS))
422
+ if content
422
423
  # First, check if the optimized layout method has already been created,
423
424
  # and use it if so. This way avoids the extra conditional and local variable
424
425
  # assignments in the next section.
@@ -518,6 +519,16 @@ class Roda
518
519
  end
519
520
  end
520
521
  end
522
+
523
+ # Get the content for #view, or return nil to use the unoptimized approach. Only called if
524
+ # a single argument is passed to view.
525
+ def _optimized_view_content(template)
526
+ if optimized_template = _cached_template_method(template)
527
+ send(optimized_template, OPTS)
528
+ elsif template.is_a?(Hash) && template.length == 1
529
+ template[:content]
530
+ end
531
+ end
521
532
  else
522
533
  # :nocov:
523
534
  def _cached_template_method(_)
@@ -535,6 +546,10 @@ class Roda
535
546
  def _optimized_render_method_for_locals(_, _)
536
547
  nil
537
548
  end
549
+
550
+ def _optimized_view_content(template)
551
+ nil
552
+ end
538
553
  # :nocov:
539
554
  end
540
555
 
@@ -51,6 +51,10 @@ class Roda
51
51
  def _cached_template_method(template)
52
52
  nil
53
53
  end
54
+
55
+ def _optimized_view_content(template)
56
+ nil
57
+ end
54
58
  end
55
59
 
56
60
  def render_locals
@@ -211,6 +211,10 @@ class Roda
211
211
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
212
212
  # OTHER DEALINGS IN THE SOFTWARE.
213
213
  module SinatraHelpers
214
+ UTF8_ENCODING = Encoding.find('UTF-8')
215
+ ISO88591_ENCODING = Encoding.find('ISO-8859-1')
216
+ BINARY_ENCODING = Encoding.find('BINARY')
217
+
214
218
  # Depend on the status_303 plugin.
215
219
  def self.load_dependencies(app, _opts = nil)
216
220
  app.plugin :status_303
@@ -432,7 +436,25 @@ class Roda
432
436
  # instructing the user agents to prompt to save.
433
437
  def attachment(filename = nil, disposition='attachment')
434
438
  if filename
435
- params = "; filename=#{File.basename(filename).inspect}"
439
+ param_filename = File.basename(filename)
440
+ encoding = param_filename.encoding
441
+
442
+ needs_encoding = param_filename.gsub!(/[^ 0-9a-zA-Z!\#$&\+\.\^_`\|~]+/, '-')
443
+ params = "; filename=#{param_filename.inspect}"
444
+
445
+ if needs_encoding && (encoding == UTF8_ENCODING || encoding == ISO88591_ENCODING)
446
+ # File name contains non attr-char characters from RFC 5987 Section 3.2.1
447
+
448
+ encoded_filename = File.basename(filename).force_encoding(BINARY_ENCODING)
449
+ # Similar regexp as above, but treat each byte separately, and encode
450
+ # space characters, since those aren't allowed in attr-char
451
+ encoded_filename.gsub!(/[^0-9a-zA-Z!\#$&\+\.\^_`\|~]/) do |c|
452
+ "%%%X" % c.ord
453
+ end
454
+
455
+ encoded_params = "; filename*=#{encoding.to_s}''#{encoded_filename}"
456
+ end
457
+
436
458
  unless @headers["Content-Type"]
437
459
  ext = File.extname(filename)
438
460
  unless ext.empty?
@@ -440,7 +462,7 @@ class Roda
440
462
  end
441
463
  end
442
464
  end
443
- @headers["Content-Disposition"] = "#{disposition}#{params}"
465
+ @headers["Content-Disposition"] = "#{disposition}#{params}#{encoded_params}"
444
466
  end
445
467
 
446
468
  # Whether or not the status is set to 1xx. Returns nil if status not yet set.
data/lib/roda/request.rb CHANGED
@@ -468,8 +468,8 @@ class Roda
468
468
  if last = rp.index('/', 1)
469
469
  @captures << rp[1, last-1]
470
470
  @remaining_path = rp[last, rp.length]
471
- elsif rp.length > 1
472
- @captures << rp[1,rp.length]
471
+ elsif (len = rp.length) > 1
472
+ @captures << rp[1, len]
473
473
  @remaining_path = ""
474
474
  end
475
475
  end
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 = 40
7
+ RodaMinorVersion = 44
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.40.0
4
+ version: 3.44.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: 2021-01-14 00:00:00.000000000 Z
11
+ date: 2021-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -211,6 +211,10 @@ extra_rdoc_files:
211
211
  - doc/release_notes/3.39.0.txt
212
212
  - doc/release_notes/3.4.0.txt
213
213
  - doc/release_notes/3.40.0.txt
214
+ - doc/release_notes/3.41.0.txt
215
+ - doc/release_notes/3.42.0.txt
216
+ - doc/release_notes/3.43.0.txt
217
+ - doc/release_notes/3.44.0.txt
214
218
  - doc/release_notes/3.5.0.txt
215
219
  - doc/release_notes/3.6.0.txt
216
220
  - doc/release_notes/3.7.0.txt
@@ -258,6 +262,10 @@ files:
258
262
  - doc/release_notes/3.39.0.txt
259
263
  - doc/release_notes/3.4.0.txt
260
264
  - doc/release_notes/3.40.0.txt
265
+ - doc/release_notes/3.41.0.txt
266
+ - doc/release_notes/3.42.0.txt
267
+ - doc/release_notes/3.43.0.txt
268
+ - doc/release_notes/3.44.0.txt
261
269
  - doc/release_notes/3.5.0.txt
262
270
  - doc/release_notes/3.6.0.txt
263
271
  - doc/release_notes/3.7.0.txt
@@ -308,6 +316,7 @@ files:
308
316
  - lib/roda/plugins/header_matchers.rb
309
317
  - lib/roda/plugins/heartbeat.rb
310
318
  - lib/roda/plugins/hooks.rb
319
+ - lib/roda/plugins/host_authorization.rb
311
320
  - lib/roda/plugins/indifferent_params.rb
312
321
  - lib/roda/plugins/json.rb
313
322
  - lib/roda/plugins/json_parser.rb
@@ -326,6 +335,7 @@ files:
326
335
  - lib/roda/plugins/named_templates.rb
327
336
  - lib/roda/plugins/not_allowed.rb
328
337
  - lib/roda/plugins/not_found.rb
338
+ - lib/roda/plugins/optimized_segment_matchers.rb
329
339
  - lib/roda/plugins/optimized_string_matchers.rb
330
340
  - lib/roda/plugins/padrino_render.rb
331
341
  - lib/roda/plugins/param_matchers.rb
@@ -339,6 +349,7 @@ files:
339
349
  - lib/roda/plugins/precompile_templates.rb
340
350
  - lib/roda/plugins/public.rb
341
351
  - lib/roda/plugins/r.rb
352
+ - lib/roda/plugins/recheck_precompiled_assets.rb
342
353
  - lib/roda/plugins/relative_path.rb
343
354
  - lib/roda/plugins/render.rb
344
355
  - lib/roda/plugins/render_each.rb
@@ -396,7 +407,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
396
407
  - !ruby/object:Gem::Version
397
408
  version: '0'
398
409
  requirements: []
399
- rubygems_version: 3.2.3
410
+ rubygems_version: 3.2.15
400
411
  signing_key:
401
412
  specification_version: 4
402
413
  summary: Routing tree web toolkit