roda 3.39.0 → 3.43.1

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: 4f01105e24f4eae80a69f5734f93d3dfe54b5f6be2489521f1d886053c569102
4
- data.tar.gz: 2aebf30460c7d5352ee15baf3f884e59dac972e0aadd63aa7ab81445a8a29f65
3
+ metadata.gz: 95c0306a50aefb2cfa3bd700403ff2d8441acb78438dcca36bdb0341708208fe
4
+ data.tar.gz: df524e11cab19b33d6f4e2a139f2813b89dc0832863aa2e0ac70c69cc4f98e68
5
5
  SHA512:
6
- metadata.gz: d3fd374b3d700106ca814fff8f2dd0c66a3295955f27a453ef5d9fb2ba7fdea583d76ce82f91efb8e2ebcbb97a07e664f3f008689d51eb67ae39aebdd86a5d0e
7
- data.tar.gz: 59c34c88ab1e5a5bc5017d46707a530c7cd002f4262c30751ba6429983b32477b813d83ada45a19873be711d1d2bd0abd95fe2a5c44c01038ad4a32dbb8f276d
6
+ metadata.gz: dc0c531cdc7d9ae5d122cae6394b49ebd9014811c6dfba64ef70bc2b4ffe03d66efe2a10dd11f180a1c68db15a7bf3e048f8fdd8550fa72584d2190d8b3d59ea
7
+ data.tar.gz: 3f42b2dd764cae91a0b81f43933c230b5795f29f33aebf18a7cbfd800071dfdb9ce29f80b471747105e6caa524571877f207cffde621fdaf3d04b89d5c295f8e
data/CHANGELOG CHANGED
@@ -1,3 +1,39 @@
1
+ = 3.43.1 (2021-04-13)
2
+
3
+ * [SECURITY] Fix issue where loading content_security_policy plugin after default_headers plugin had no effect (jeremyevans)
4
+
5
+ = 3.43.0 (2021-04-12)
6
+
7
+ * Add host_authorization plugin, for checking that requests are submitted using an approved host (jeremyevans)
8
+
9
+ = 3.42.0 (2021-03-12)
10
+
11
+ * Make Roda.plugin support plugins using keyword arguments in Ruby 3 (jeremyevans)
12
+
13
+ * Make Roda.use support middleware using keyword arguments in Ruby 3 (pat) (#207)
14
+
15
+ * Support common_logger plugin :method option for specifying the method to call on the logger (fnordfish, jeremyevans) (#206)
16
+
17
+ * Add recheck_precompiled_assets plugin for checking for updates to the precompiled asset metadata file (jeremyevans)
18
+
19
+ * Make compile_assets class method in assets plugin use an atomic approach to writing precompiled metadata file (jeremyevans)
20
+
21
+ = 3.41.0 (2021-02-17)
22
+
23
+ * Improve view performance with :content option up to 3x by calling compiled template methods directly (jeremyevans)
24
+
25
+ = 3.40.0 (2021-01-14)
26
+
27
+ * Add freeze_template_caches! to the precompile_templates plugin, which ensures all templates are precompiled, and speeds up template access (jeremyevans)
28
+
29
+ * Add precompile_views to the precompile_templates plugin, which precompiles the optimized render methods (jeremyevans)
30
+
31
+ * Have RodaCache#freeze return the frozen internal hash (which no longer needs a mutex for thread-safety) (jeremyevans)
32
+
33
+ * Speed up the view method in the render plugin even more when freezing the application (jeremyevans)
34
+
35
+ * Speed up the view method in the render plugin when called with a single argument (jeremyevans)
36
+
1
37
  = 3.39.0 (2020-12-15)
2
38
 
3
39
  * Speed up relative_path plugin if relative_path or relative_prefix is called more than once (jeremyevans)
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014-2020 Jeremy Evans
1
+ Copyright (c) 2014-2021 Jeremy Evans
2
2
  Copyright (c) 2010-2014 Michel Martens, Damian Janowski and Cyril David
3
3
  Copyright (c) 2008-2009 Christian Neukirchen
4
4
 
@@ -0,0 +1,24 @@
1
+ = New Features
2
+
3
+ * A precompile_views method has been added to the
4
+ precompile_templates plugin. This method works with Roda's
5
+ optimized compiled view methods, allowing additional memory
6
+ sharing between parent and child processes.
7
+
8
+ * A freeze_template_caches! method has been added to the
9
+ precompile_templates plugin. This freezes the template caches,
10
+ preventing the compilation of additional templates, useful for
11
+ enforcing that only precompiled templates are used. Additionally,
12
+ this speeds up access to the template caches.
13
+
14
+ * RodaCache#freeze now returns the frozen internal hash, which can
15
+ then be accessed without a mutex. Previously, freeze only froze
16
+ the receiver and not the internal hash, so it didn't have the
17
+ expected effect.
18
+
19
+ = Other Improvements
20
+
21
+ * The view method in the render plugin is now faster in most cases
22
+ when a single argument is used. When freezing the application,
23
+ an additional optimization is performed to increase the
24
+ performance of the view method even further.
@@ -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
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
 
data/lib/roda/cache.rb CHANGED
@@ -22,6 +22,13 @@ class Roda
22
22
  @mutex.synchronize{@hash[key] = value}
23
23
  end
24
24
 
25
+ # Return the frozen internal hash. The internal hash can then
26
+ # be accessed directly since it is frozen and there are no
27
+ # thread safety issues.
28
+ def freeze
29
+ @hash.freeze
30
+ end
31
+
25
32
  private
26
33
 
27
34
  # Create a copy of the cache with a separate mutex.
@@ -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
+
@@ -13,32 +13,33 @@ class Roda
13
13
  # all of the child processes can use the same precompiled templates, which
14
14
  # saves memory.
15
15
  #
16
- # After loading the plugin, you can call +precompile_templates+ with
17
- # the pattern of templates you would like to precompile:
16
+ # Another advantage of the precompile_templates plugin is that after
17
+ # template precompilation, access to the template file in the file system is
18
+ # no longer needed, so this can be used with security features that do not
19
+ # allow access to the template files at runtime.
20
+ #
21
+ # After loading the plugin, you should call precompile_views with an array
22
+ # of views to compile, using the same argument you are passing to view or
23
+ # render:
18
24
  #
19
25
  # plugin :precompile_templates
20
- # precompile_templates "views/\*\*/*.erb"
26
+ # precompile_views %w'view1 view2'
27
+ #
28
+ # If the view requires local variables, you should call precompile_views with a second
29
+ # argument for the local variables:
21
30
  #
22
- # That will precompile all erb template files in the views directory or
23
- # any subdirectory.
31
+ # plugin :precompile_templates
32
+ # precompile_views :view3, [:local_var1, :local_var2]
24
33
  #
25
- # If the templates use local variables, you need to specify which local
26
- # variables to precompile, which should be an array of symbols:
34
+ # After all templates are precompiled, you can optionally use freeze_template_caches!,
35
+ # which will freeze the template caches so that any template compilation at runtime
36
+ # will result in an error. This also speeds up template cache access, since the
37
+ # template caches no longer need a mutex.
27
38
  #
28
- # precompile_templates 'views/users/_*.erb', locals: [:user]
39
+ # freeze_template_caches!
29
40
  #
30
41
  # Note that you should use Tilt 2.0.1+ if you are using this plugin, so
31
42
  # that locals are handled in the same order.
32
- #
33
- # You can specify other render options when calling +precompile_templates+,
34
- # including +:cache_key+, +:template_class+, and +:template_opts+. If you
35
- # are passing any of those options to render/view for the template, you
36
- # should pass the same options when precompiling the template.
37
- #
38
- # To compile inline templates, just pass a single hash containing an :inline
39
- # to +precompile_templates+:
40
- #
41
- # precompile_templates inline: some_template_string
42
43
  module PrecompileTemplates
43
44
  # Load the render plugin as precompile_templates depends on it.
44
45
  def self.load_dependencies(app, opts=OPTS)
@@ -46,8 +47,49 @@ class Roda
46
47
  end
47
48
 
48
49
  module ClassMethods
49
- # Precompile the templates using the given options. See PrecompileTemplates
50
- # for details.
50
+ # Freeze the template caches. Should be called after precompiling all templates during
51
+ # application startup, if you don't want to allow templates to be cached at runtime.
52
+ # In addition to ensuring that no templates are compiled at runtime, this also speeds
53
+ # up rendering by freezing the template caches, so that a mutex is not needed to access
54
+ # them.
55
+ def freeze_template_caches!
56
+ _freeze_layout_method
57
+
58
+ opts[:render] = render_opts.merge(
59
+ :cache=>render_opts[:cache].freeze,
60
+ :template_method_cache=>render_opts[:template_method_cache].freeze,
61
+ ).freeze
62
+ self::RodaCompiledTemplates.freeze
63
+
64
+ nil
65
+ end
66
+
67
+ # Precompile the templates using the given options. Note that this doesn't
68
+ # handle optimized template methods supported in newer versions of Roda, but
69
+ # there are still cases where makes sense to use it.
70
+ #
71
+ # You can call +precompile_templates+ with the pattern of templates you would
72
+ # like to precompile:
73
+ #
74
+ # precompile_templates "views/**/*.erb"
75
+ #
76
+ # That will precompile all erb template files in the views directory or
77
+ # any subdirectory.
78
+ #
79
+ # If the templates use local variables, you need to specify which local
80
+ # variables to precompile, which should be an array of symbols:
81
+ #
82
+ # precompile_templates 'views/users/_*.erb', locals: [:user]
83
+ #
84
+ # You can specify other render options when calling +precompile_templates+,
85
+ # including +:cache_key+, +:template_class+, and +:template_opts+. If you
86
+ # are passing any of those options to render/view for the template, you
87
+ # should pass the same options when precompiling the template.
88
+ #
89
+ # To compile inline templates, just pass a single hash containing an :inline
90
+ # to +precompile_templates+:
91
+ #
92
+ # precompile_templates inline: some_template_string
51
93
  def precompile_templates(pattern, opts=OPTS)
52
94
  if pattern.is_a?(Hash)
53
95
  opts = pattern.merge(opts)
@@ -68,7 +110,40 @@ class Roda
68
110
  instance = allocate
69
111
  compile_opts.each do |compile_opt|
70
112
  template = instance.send(:retrieve_template, compile_opt)
71
- Render.tilt_template_compiled_method(template, locals, self)
113
+ begin
114
+ Render.tilt_template_compiled_method(template, locals, self)
115
+ rescue NotImplementedError
116
+ # When freezing template caches, you may want to precompile a template for a
117
+ # template type that doesn't support template precompilation, just to populate
118
+ # the cache. Tilt rescues NotImplementedError in this case, which we can ignore.
119
+ nil
120
+ end
121
+ end
122
+
123
+ nil
124
+ end
125
+
126
+ # Precompile the given views with the given locals, handling optimized template methods.
127
+ def precompile_views(views, locals=EMPTY_ARRAY)
128
+ instance = allocate
129
+ views = Array(views)
130
+
131
+ if locals.empty?
132
+ opts = OPTS
133
+ else
134
+ locals_hash = {}
135
+ locals.each{|k| locals_hash[k] = nil}
136
+ opts = {:locals=>locals_hash}
137
+ end
138
+
139
+ views.each do |view|
140
+ instance.send(:retrieve_template, instance.send(:render_template_opts, view, opts))
141
+ end
142
+
143
+ if locals_hash
144
+ views.each do |view|
145
+ instance.send(:_optimized_render_method_for_locals, view, locals_hash)
146
+ end
72
147
  end
73
148
 
74
149
  nil
@@ -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
@@ -333,6 +334,25 @@ class Roda
333
334
  end
334
335
 
335
336
  module ClassMethods
337
+ # :nocov:
338
+ if COMPILED_METHOD_SUPPORT
339
+ # :nocov:
340
+ # If using compiled methods and there is an optimized layout, speed up
341
+ # access to the layout method to improve the performance of view.
342
+ def freeze
343
+ begin
344
+ _freeze_layout_method
345
+ rescue
346
+ # This is only for optimization, if any errors occur, they can be ignored.
347
+ # One possibility for error is the app doesn't use a layout, but doesn't
348
+ # specifically set the :layout=>false plugin option.
349
+ nil
350
+ end
351
+
352
+ super
353
+ end
354
+ end
355
+
336
356
  # Copy the rendering options into the subclass, duping
337
357
  # them as necessary to prevent changes in the subclass
338
358
  # affecting the parent class.
@@ -352,6 +372,29 @@ class Roda
352
372
  def render_opts
353
373
  opts[:render]
354
374
  end
375
+
376
+ private
377
+
378
+ # Precompile the layout method, to reduce method calls to look it up at runtime.
379
+ def _freeze_layout_method
380
+ if render_opts[:layout]
381
+ instance = allocate
382
+ instance.send(:retrieve_template, instance.send(:view_layout_opts, OPTS))
383
+
384
+ # :nocov:
385
+ if COMPILED_METHOD_SUPPORT
386
+ # :nocov:
387
+ if (layout_template = render_opts[:optimize_layout]) && !opts[:render][:optimized_layout_method_created]
388
+ instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
389
+ layout_method = opts[:render][:template_method_cache][:_roda_layout]
390
+ define_method(:_layout_method){layout_method}
391
+ private :_layout_method
392
+ alias_method(:_layout_method, :_layout_method)
393
+ opts[:render] = opts[:render].merge(:optimized_layout_method_created=>true)
394
+ end
395
+ end
396
+ end
397
+ end
355
398
  end
356
399
 
357
400
  module InstanceMethods
@@ -375,19 +418,21 @@ class Roda
375
418
  # Render the given template. If there is a default layout
376
419
  # for the class, take the result of the template rendering
377
420
  # and render it inside the layout. See Render for details.
378
- def view(template, opts = (optimized_template = _cached_template_method(template); OPTS))
379
- if optimized_template
380
- content = send(optimized_template, OPTS)
381
-
382
- render_opts = self.class.opts[:render]
383
- if layout_template = render_opts[:optimize_layout]
384
- method_cache = render_opts[:template_method_cache]
385
- unless layout_method = method_cache[:_roda_layout]
386
- retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
387
- layout_method = method_cache[:_roda_layout]
388
- end
421
+ def view(template, opts = (content = _optimized_view_content(template); OPTS))
422
+ if content
423
+ # First, check if the optimized layout method has already been created,
424
+ # and use it if so. This way avoids the extra conditional and local variable
425
+ # assignments in the next section.
426
+ if layout_method = _layout_method
427
+ return send(layout_method, OPTS){content}
428
+ end
389
429
 
390
- if layout_method
430
+ # If we have an optimized template method but no optimized layout method, create the
431
+ # optimized layout method if possible and use it. If you can't create the optimized
432
+ # layout method, fall through to the slower approach.
433
+ if layout_template = self.class.opts[:render][:optimize_layout]
434
+ retrieve_template(:template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
435
+ if layout_method = _layout_method
391
436
  return send(layout_method, OPTS){content}
392
437
  end
393
438
  end
@@ -428,6 +473,11 @@ class Roda
428
473
  method_cache[template]
429
474
  end
430
475
 
476
+ # Return a symbol containing the optimized layout method
477
+ def _layout_method
478
+ self.class.opts[:render][:template_method_cache][:_roda_layout]
479
+ end
480
+
431
481
  # Use an optimized render path for templates with a hash of locals. Returns the result
432
482
  # of the template render if the optimized path is used, or nil if the optimized
433
483
  # path is not used and the long method needs to be used.
@@ -469,19 +519,37 @@ class Roda
469
519
  end
470
520
  end
471
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
472
532
  else
473
533
  # :nocov:
474
- def _cached_template_method(template)
534
+ def _cached_template_method(_)
475
535
  nil
476
536
  end
477
537
 
478
- def _cached_template_method_key(template)
538
+ def _cached_template_method_key(_)
539
+ nil
540
+ end
541
+
542
+ def _layout_method
479
543
  nil
480
544
  end
481
545
 
482
546
  def _optimized_render_method_for_locals(_, _)
483
547
  nil
484
548
  end
549
+
550
+ def _optimized_view_content(template)
551
+ nil
552
+ end
485
553
  # :nocov:
486
554
  end
487
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
data/lib/roda/version.rb CHANGED
@@ -4,11 +4,11 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 39
7
+ RodaMinorVersion = 43
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
11
- RodaPatchVersion = 0
11
+ RodaPatchVersion = 1
12
12
 
13
13
  # The full version of Roda as a string.
14
14
  RodaVersion = "#{RodaMajorVersion}.#{RodaMinorVersion}.#{RodaPatchVersion}".freeze
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.39.0
4
+ version: 3.43.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-12-15 00:00:00.000000000 Z
11
+ date: 2021-04-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -174,16 +174,8 @@ extra_rdoc_files:
174
174
  - MIT-LICENSE
175
175
  - CHANGELOG
176
176
  - doc/conventions.rdoc
177
- - doc/release_notes/3.7.0.txt
178
177
  - doc/release_notes/3.0.0.txt
179
178
  - doc/release_notes/3.1.0.txt
180
- - doc/release_notes/3.2.0.txt
181
- - doc/release_notes/3.3.0.txt
182
- - doc/release_notes/3.4.0.txt
183
- - doc/release_notes/3.5.0.txt
184
- - doc/release_notes/3.6.0.txt
185
- - doc/release_notes/3.8.0.txt
186
- - doc/release_notes/3.9.0.txt
187
179
  - doc/release_notes/3.10.0.txt
188
180
  - doc/release_notes/3.11.0.txt
189
181
  - doc/release_notes/3.12.0.txt
@@ -195,6 +187,7 @@ extra_rdoc_files:
195
187
  - doc/release_notes/3.17.0.txt
196
188
  - doc/release_notes/3.18.0.txt
197
189
  - doc/release_notes/3.19.0.txt
190
+ - doc/release_notes/3.2.0.txt
198
191
  - doc/release_notes/3.20.0.txt
199
192
  - doc/release_notes/3.21.0.txt
200
193
  - doc/release_notes/3.22.0.txt
@@ -205,6 +198,7 @@ extra_rdoc_files:
205
198
  - doc/release_notes/3.27.0.txt
206
199
  - doc/release_notes/3.28.0.txt
207
200
  - doc/release_notes/3.29.0.txt
201
+ - doc/release_notes/3.3.0.txt
208
202
  - doc/release_notes/3.30.0.txt
209
203
  - doc/release_notes/3.31.0.txt
210
204
  - doc/release_notes/3.32.0.txt
@@ -215,6 +209,16 @@ extra_rdoc_files:
215
209
  - doc/release_notes/3.37.0.txt
216
210
  - doc/release_notes/3.38.0.txt
217
211
  - doc/release_notes/3.39.0.txt
212
+ - doc/release_notes/3.4.0.txt
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.5.0.txt
218
+ - doc/release_notes/3.6.0.txt
219
+ - doc/release_notes/3.7.0.txt
220
+ - doc/release_notes/3.8.0.txt
221
+ - doc/release_notes/3.9.0.txt
218
222
  files:
219
223
  - CHANGELOG
220
224
  - MIT-LICENSE
@@ -256,6 +260,10 @@ files:
256
260
  - doc/release_notes/3.38.0.txt
257
261
  - doc/release_notes/3.39.0.txt
258
262
  - doc/release_notes/3.4.0.txt
263
+ - doc/release_notes/3.40.0.txt
264
+ - doc/release_notes/3.41.0.txt
265
+ - doc/release_notes/3.42.0.txt
266
+ - doc/release_notes/3.43.0.txt
259
267
  - doc/release_notes/3.5.0.txt
260
268
  - doc/release_notes/3.6.0.txt
261
269
  - doc/release_notes/3.7.0.txt
@@ -306,6 +314,7 @@ files:
306
314
  - lib/roda/plugins/header_matchers.rb
307
315
  - lib/roda/plugins/heartbeat.rb
308
316
  - lib/roda/plugins/hooks.rb
317
+ - lib/roda/plugins/host_authorization.rb
309
318
  - lib/roda/plugins/indifferent_params.rb
310
319
  - lib/roda/plugins/json.rb
311
320
  - lib/roda/plugins/json_parser.rb
@@ -337,6 +346,7 @@ files:
337
346
  - lib/roda/plugins/precompile_templates.rb
338
347
  - lib/roda/plugins/public.rb
339
348
  - lib/roda/plugins/r.rb
349
+ - lib/roda/plugins/recheck_precompiled_assets.rb
340
350
  - lib/roda/plugins/relative_path.rb
341
351
  - lib/roda/plugins/render.rb
342
352
  - lib/roda/plugins/render_each.rb
@@ -394,7 +404,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
394
404
  - !ruby/object:Gem::Version
395
405
  version: '0'
396
406
  requirements: []
397
- rubygems_version: 3.1.4
407
+ rubygems_version: 3.2.15
398
408
  signing_key:
399
409
  specification_version: 4
400
410
  summary: Routing tree web toolkit