mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add caching? helper method
Caching something that shouldn't be cached is a potential source of bugs and security vulnerabilities. For example, one could write a form helper that outputs a request-specific auth token, only for the helper to be used inside of a `cache` block. In the GitHub application, we implemented a caching? method and used it to raise an error if a specific code path is being cached that we don't want to be cached. I've credited its original author, @btoews. Co-authored-by: Ben Toews <mastahyeti@gmail.com> Co-authored-by: John Hawthorn <jhawthorn@github.com> Co-authored-by: Kasper Timm Hansen <kaspth@gmail.com>
This commit is contained in:
parent
18707ab17f
commit
fd5792f0fc
6 changed files with 79 additions and 2 deletions
|
@ -1,3 +1,7 @@
|
|||
* Add `caching?` helper that returns whether the current code path is being cached and `unacheable!` to denote helper methods that can't participate in fragment caching.
|
||||
|
||||
*Ben Toews*, *John Hawthorn*, *Kasper Timm Hansen*, *Joel Hawksley*
|
||||
|
||||
* Add `include_seconds` option for `time_field`
|
||||
|
||||
<%= form.time_field :foo, include_seconds: false %>
|
||||
|
|
|
@ -4,6 +4,8 @@ module ActionView
|
|||
# = Action View Cache Helper
|
||||
module Helpers # :nodoc:
|
||||
module CacheHelper
|
||||
class UncacheableFragmentError < StandardError; end
|
||||
|
||||
# This helper exposes a method for caching fragments of a view
|
||||
# rather than an entire action or page. This technique is useful
|
||||
# caching pieces like menus, lists of new topics, static HTML
|
||||
|
@ -165,8 +167,10 @@ module ActionView
|
|||
# expire the cache.
|
||||
def cache(name = {}, options = {}, &block)
|
||||
if controller.respond_to?(:perform_caching) && controller.perform_caching
|
||||
CachingRegistry.track_caching do
|
||||
name_options = options.slice(:skip_digest)
|
||||
safe_concat(fragment_for(cache_fragment_name(name, **name_options), options, &block))
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
|
@ -174,6 +178,34 @@ module ActionView
|
|||
nil
|
||||
end
|
||||
|
||||
# Returns whether the current view fragment is within a +cache+ block.
|
||||
#
|
||||
# Useful when certain fragments aren't cacheable:
|
||||
#
|
||||
# <% cache project do %>
|
||||
# <% raise StandardError, "Caching private data!" if caching? %>
|
||||
# <% end %>
|
||||
def caching?
|
||||
CachingRegistry.caching?
|
||||
end
|
||||
|
||||
# Raises +UncacheableFragmentError+ when called from within a +cache+ block.
|
||||
#
|
||||
# Useful to denote helper methods that can't participate in fragment caching:
|
||||
#
|
||||
# def project_name_with_time(project)
|
||||
# uncacheable!
|
||||
# "#{project.name} - #{Time.now}"
|
||||
# end
|
||||
#
|
||||
# # Which will then raise if used within a +cache+ block:
|
||||
# <% cache project do %>
|
||||
# <%= project_name_with_time(project) %>
|
||||
# <% end %>
|
||||
def uncacheable!
|
||||
raise UncacheableFragmentError, "can't be fragment cached" if caching?
|
||||
end
|
||||
|
||||
# Cache fragments of a view if +condition+ is true
|
||||
#
|
||||
# <% cache_if admin?, project do %>
|
||||
|
@ -259,6 +291,22 @@ module ActionView
|
|||
end
|
||||
controller.write_fragment(name, fragment, options)
|
||||
end
|
||||
|
||||
class CachingRegistry
|
||||
extend ActiveSupport::PerThreadRegistry
|
||||
|
||||
attr_accessor :caching
|
||||
alias caching? caching
|
||||
|
||||
def self.track_caching
|
||||
caching_was = self.caching
|
||||
self.caching = true
|
||||
|
||||
yield
|
||||
ensure
|
||||
self.caching = caching_was
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
3
actionview/test/fixtures/test/caching_predicate.html.erb
vendored
Normal file
3
actionview/test/fixtures/test/caching_predicate.html.erb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<%= cache "foo" do %>
|
||||
<%= "Cached!" if caching? %>
|
||||
<% end %>
|
1
actionview/test/fixtures/test/caching_predicate_outside_cache.html.erb
vendored
Normal file
1
actionview/test/fixtures/test/caching_predicate_outside_cache.html.erb
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
<%= "Not cached!" unless caching? %>
|
3
actionview/test/fixtures/test/uncacheable.html.erb
vendored
Normal file
3
actionview/test/fixtures/test/uncacheable.html.erb
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<%= cache "uhoh" do %>
|
||||
<%= uncacheable! %>
|
||||
<% end %>
|
|
@ -721,6 +721,24 @@ class CachedViewRenderTest < ActiveSupport::TestCase
|
|||
|
||||
assert_not_equal cat, dog
|
||||
end
|
||||
|
||||
def test_caching_predicate_method
|
||||
result = @view.render(template: "test/caching_predicate")
|
||||
|
||||
assert_match "Cached!", result
|
||||
end
|
||||
|
||||
def test_caching_predicate_method_outside_of_cache
|
||||
result = @view.render(template: "test/caching_predicate_outside_cache")
|
||||
|
||||
assert_match "Not cached!", result
|
||||
end
|
||||
|
||||
def test_uncacheable
|
||||
e = assert_raises(ActionView::Template::Error) { @view.render(template: "test/uncacheable") }
|
||||
|
||||
assert_match "can't be fragment cached", e.cause.message
|
||||
end
|
||||
end
|
||||
|
||||
class LazyViewRenderTest < ActiveSupport::TestCase
|
||||
|
|
Loading…
Reference in a new issue