1
0
Fork 0
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:
Joel Hawksley 2021-06-02 16:29:59 -06:00
parent 18707ab17f
commit fd5792f0fc
No known key found for this signature in database
GPG key ID: 1BBA4349888F4C0D
6 changed files with 79 additions and 2 deletions

View file

@ -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 %>

View file

@ -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

View file

@ -0,0 +1,3 @@
<%= cache "foo" do %>
<%= "Cached!" if caching? %>
<% end %>

View file

@ -0,0 +1 @@
<%= "Not cached!" unless caching? %>

View file

@ -0,0 +1,3 @@
<%= cache "uhoh" do %>
<%= uncacheable! %>
<% end %>

View file

@ -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