diff options
Diffstat (limited to 'actionview')
36 files changed, 407 insertions, 134 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index 420136d6de..be2a662adc 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -196,10 +196,9 @@ module ActionView #:nodoc: end end - attr_reader :view_renderer + attr_reader :view_renderer, :lookup_context attr_internal :config, :assigns - delegate :lookup_context, to: :view_renderer delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, to: :lookup_context def assign(new_assigns) # :nodoc: @@ -208,12 +207,17 @@ module ActionView #:nodoc: # :stopdoc: - def self.build_renderer(context, controller, formats) - lookup_context = context.is_a?(ActionView::LookupContext) ? - context : ActionView::LookupContext.new(context) - lookup_context.formats = formats if formats - lookup_context.prefixes = controller._prefixes if controller - ActionView::Renderer.new(lookup_context) + def self.build_lookup_context(context) + case context + when ActionView::Renderer + context.lookup_context + when Array + ActionView::LookupContext.new(context) + when nil + ActionView::LookupContext.new([]) + else + raise NotImplementedError, context.class.name + end end def self.empty @@ -225,54 +229,80 @@ module ActionView #:nodoc: end def self.with_context(context, assigns = {}, controller = nil) - new ActionView::Renderer.new(context), assigns, controller + new context, assigns, controller end NULL = Object.new # :startdoc: - def initialize(renderer = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc: + def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc: @_config = ActiveSupport::InheritableOptions.new - if formats == NULL - formats = nil - else + unless formats == NULL ActiveSupport::Deprecation.warn <<~eowarn Passing formats to ActionView::Base.new is deprecated eowarn end - if renderer.is_a?(ActionView::Renderer) - @view_renderer = renderer + case lookup_context + when ActionView::LookupContext + @lookup_context = lookup_context else ActiveSupport::Deprecation.warn <<~eowarn - ActionView::Base instances should be constructed with a view renderer, + ActionView::Base instances should be constructed with a lookup context, assigments, and a controller. eowarn - @view_renderer = self.class.build_renderer(renderer, controller, formats) + @lookup_context = self.class.build_lookup_context(lookup_context) end + @view_renderer = ActionView::Renderer.new @lookup_context + @current_template = nil + @cache_hit = {} assign(assigns) assign_controller(controller) _prepare_context end - def run(method, locals, buffer, &block) - _old_output_buffer, _old_virtual_path = @output_buffer, @virtual_path + def run(method, template, locals, buffer, &block) + _old_output_buffer, _old_virtual_path, _old_template = @output_buffer, @virtual_path, @current_template + @current_template = template @output_buffer = buffer send(method, locals, buffer, &block) ensure - @output_buffer, @virtual_path = _old_output_buffer, _old_virtual_path + @output_buffer, @virtual_path, @current_template = _old_output_buffer, _old_virtual_path, _old_template end def compiled_method_container - raise NotImplementedError, <<~msg - Subclasses of ActionView::Base must implement `compiled_method_container` - or use the class method `with_empty_template_cache` for constructing - an ActionView::Base subclass that has an empty cache. - msg + if self.class == ActionView::Base + ActiveSupport::Deprecation.warn <<~eowarn + ActionView::Base instances must implement `compiled_method_container` + or use the class method `with_empty_template_cache` for constructing + an ActionView::Base instances that has an empty cache. + eowarn + end + + self.class + end + + def in_rendering_context(options) + old_view_renderer = @view_renderer + old_lookup_context = @lookup_context + + if !lookup_context.html_fallback_for_js && options[:formats] + formats = Array(options[:formats]) + if formats == [:js] + formats << :html + end + @lookup_context = lookup_context.with_prepended_formats(formats) + @view_renderer = ActionView::Renderer.new @lookup_context + end + + yield @view_renderer + ensure + @view_renderer = old_view_renderer + @lookup_context = old_lookup_context end ActiveSupport.run_load_hooks(:action_view, self) diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 6d2e471a44..9fa8d7eab1 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -18,11 +18,11 @@ module ActionView # * <tt>name</tt> - Template name # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views - def digest(name:, finder:, dependencies: nil) + def digest(name:, format:, finder:, dependencies: nil) if dependencies.nil? || dependencies.empty? - cache_key = "#{name}.#{finder.rendered_format}" + cache_key = "#{name}.#{format}" else - cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + cache_key = [ name, format, dependencies ].flatten.compact.join(".") end # this is a correctly done double-checked locking idiom @@ -48,8 +48,6 @@ module ActionView logical_name = name.gsub(%r|/_|, "/") if template = find_template(finder, logical_name, [], partial, []) - finder.rendered_format ||= template.formats.first - if node = seen[template.identifier] # handle cycles in the tree node else @@ -73,9 +71,7 @@ module ActionView private def find_template(finder, name, prefixes, partial, keys) finder.disable_cache do - format = finder.rendered_format - result = finder.find_all(name, prefixes, partial, keys, formats: [format]).first if format - result || finder.find_all(name, prefixes, partial, keys).first + finder.find_all(name, prefixes, partial, keys).first end end end diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index b1a14250c3..6b69b71947 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -216,13 +216,13 @@ module ActionView end end - def digest_path_from_virtual(virtual_path) # :nodoc: - digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies) + def digest_path_from_template(template) # :nodoc: + digest = Digestor.digest(name: template.virtual_path, format: template.formats.first, finder: lookup_context, dependencies: view_cache_dependencies) if digest.present? - "#{virtual_path}:#{digest}" + "#{template.virtual_path}:#{digest}" else - virtual_path + template.virtual_path end end @@ -234,7 +234,7 @@ module ActionView if virtual_path || digest_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - digest_path ||= digest_path_from_virtual(virtual_path) + digest_path ||= digest_path_from_template(@current_template) [ digest_path, name ] else diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb index e2e065c218..4415018845 100644 --- a/actionview/lib/action_view/helpers/csp_helper.rb +++ b/actionview/lib/action_view/helpers/csp_helper.rb @@ -14,9 +14,11 @@ module ActionView # This is used by the Rails UJS helper to create dynamically # loaded inline <script> elements. # - def csp_meta_tag + def csp_meta_tag(**options) if content_security_policy? - tag("meta", name: "csp-nonce", content: content_security_policy_nonce) + options[:name] = "csp-nonce" + options[:content] = content_security_policy_nonce + tag("meta", options) end end end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 1e12aa2736..7ead691113 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -27,10 +27,12 @@ module ActionView def render(options = {}, locals = {}, &block) case options when Hash - if block_given? - view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block) - else - view_renderer.render(self, options) + in_rendering_context(options) do |renderer| + if block_given? + view_renderer.render_partial(self, options.merge(partial: options[:layout]), &block) + else + view_renderer.render(self, options) + end end else view_renderer.render_partial(self, partial: options, locals: locals, &block) diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index c3bb0a49fc..10cd61bbd6 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -16,6 +16,8 @@ module ActionView # only once during the request, it speeds up all cache accesses. class LookupContext #:nodoc: attr_accessor :prefixes, :rendered_format + deprecate :rendered_format + deprecate :rendered_format= mattr_accessor :fallbacks, default: FallbackFileSystemResolver.instances @@ -58,22 +60,32 @@ module ActionView alias :eql? :equal? @details_keys = Concurrent::Map.new + @digest_cache = Concurrent::Map.new - def self.get(details) + def self.digest_cache(details) + @digest_cache[details_cache_key(details)] ||= Concurrent::Map.new + end + + def self.details_cache_key(details) if details[:formats] details = details.dup details[:formats] &= Template::Types.symbols end - @details_keys[details] ||= Concurrent::Map.new + @details_keys[details] ||= Object.new end def self.clear + ActionView::ViewPaths.all_view_paths.each do |path_set| + path_set.each(&:clear_cache) + end + ActionView::LookupContext.fallbacks.each(&:clear_cache) @view_context_class = nil @details_keys.clear + @digest_cache.clear end def self.digest_caches - @details_keys.values + @digest_cache.values end def self.view_context_class(klass) @@ -88,7 +100,7 @@ module ActionView # Calculate the details key. Remove the handlers from calculation to improve performance # since the user cannot modify it explicitly. def details_key #:nodoc: - @details_key ||= DetailsKey.get(@details) if @cache + @details_key ||= DetailsKey.details_cache_key(@details) if @cache end # Temporary skip passing the details_key forward. @@ -102,7 +114,8 @@ module ActionView private def _set_detail(key, value) # :doc: - @details = @details.dup if @details_key + @details = @details.dup if @digest_cache || @details_key + @digest_cache = nil @details_key = nil @details[key] = value end @@ -178,7 +191,7 @@ module ActionView user_details = @details.merge(options) if @cache - details_key = DetailsKey.get(user_details) + details_key = DetailsKey.details_cache_key(user_details) else details_key = nil end @@ -205,7 +218,7 @@ module ActionView end if @cache - [details, DetailsKey.get(details)] + [details, DetailsKey.details_cache_key(details)] else [details, nil] end @@ -236,16 +249,23 @@ module ActionView def initialize(view_paths, details = {}, prefixes = []) @details_key = nil + @digest_cache = nil @cache = true @prefixes = prefixes - @rendered_format = nil @details = initialize_details({}, details) @view_paths = build_view_paths(view_paths) end def digest_cache - details_key + @digest_cache ||= DetailsKey.digest_cache(@details) + end + + def with_prepended_formats(formats) + details = @details.dup + details[:formats] = formats + + self.class.new(@view_paths, details, @prefixes) end def initialize_details(target, details) diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index ae366ce54a..200dc3e10e 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -27,6 +27,46 @@ module ActionView raise NotImplementedError end + class RenderedCollection # :nodoc: + attr_reader :rendered_templates + + def initialize(rendered_templates, spacer) + @rendered_templates = rendered_templates + @spacer = spacer + end + + def body + @rendered_templates.map(&:body).join(@spacer.body).html_safe + end + + def format + rendered_templates.first.format + end + + class EmptyCollection + def format; nil; end + def body; nil; end + end + + EMPTY = EmptyCollection.new + end + + class RenderedTemplate # :nodoc: + attr_reader :body, :layout, :template + + def initialize(body, layout, template) + @body = body + @layout = layout + @template = template + end + + def format + template.formats.first + end + + EMPTY_SPACER = Struct.new(:body).new + end + private def extract_details(options) # :doc: @@ -49,5 +89,13 @@ module ActionView @lookup_context.formats = formats | @lookup_context.formats end + + def build_rendered_template(content, template, layout = nil) + RenderedTemplate.new content, layout, template + end + + def build_rendered_collection(templates, spacer) + RenderedCollection.new templates, spacer + end end end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 801916954f..4ae6f635ae 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -308,15 +308,10 @@ module ActionView template = find_partial(@path, @template_keys) @variable ||= template.variable else - template = nil - end - - @lookup_context.rendered_format ||= begin - if template && template.formats.first - template.formats.first - else - formats.first + if options[:cached] + raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering" end + template = nil end if @collection @@ -331,15 +326,23 @@ module ActionView def render_collection(view, template) identifier = (template && template.identifier) || @path instrument(:collection, identifier: identifier, count: @collection.size) do |payload| - return nil if @collection.blank? + return RenderedCollection::EMPTY if @collection.blank? - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals) + spacer = if @options.key?(:spacer_template) + spacer_template = find_template(@options[:spacer_template], @locals.keys) + build_rendered_template(spacer_template.render(view, @locals), spacer_template) + else + RenderedTemplate::EMPTY_SPACER end - cache_collection_render(payload, view, template) do - template ? collection_with_template(view, template) : collection_without_template(view) - end.join(spacer).html_safe + collection_body = if template + cache_collection_render(payload, view, template) do + collection_with_template(view, template) + end + else + collection_without_template(view) + end + build_rendered_collection(collection_body, spacer) end end @@ -361,7 +364,7 @@ module ActionView content = layout.render(view, locals) { content } if layout payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] - content + build_rendered_template(content, template, layout) end end @@ -379,8 +382,6 @@ module ActionView @locals = options[:locals] || {} @details = extract_details(options) - prepend_formats(options[:formats]) - partial = options[:partial] if String === partial @@ -454,7 +455,7 @@ module ActionView content = template.render(view, locals) content = layout.render(view, locals) { content } if layout partial_iteration.iterate! - content + build_rendered_template(content, template, layout) end end @@ -476,7 +477,7 @@ module ActionView template = (cache[path] ||= find_template(path, keys + [as, counter, iteration])) content = template.render(view, locals) partial_iteration.iterate! - content + build_rendered_template(content, template) end end diff --git a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb index 388f9e5e56..ed59033e27 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -40,7 +40,7 @@ module ActionView rendered_partials = @collection.empty? ? [] : yield index = 0 - fetch_or_cache_partial(cached_partials, order_by: keyed_collection.each_key) do + fetch_or_cache_partial(cached_partials, template, order_by: keyed_collection.each_key) do # This block is called once # for every cache miss while preserving order. rendered_partials[index].tap { index += 1 } @@ -54,7 +54,7 @@ module ActionView def collection_by_cache_keys(view, template) seed = callable_cache_key? ? @options[:cached] : ->(i) { i } - digest_path = view.digest_path_from_virtual(template.virtual_path) + digest_path = view.digest_path_from_template(template) @collection.each_with_object({}) do |item, hash| hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item @@ -81,11 +81,13 @@ module ActionView # # If the partial is not already cached it will also be # written back to the underlying cache store. - def fetch_or_cache_partial(cached_partials, order_by:) + def fetch_or_cache_partial(cached_partials, template, order_by:) order_by.map do |cache_key| - cached_partials.fetch(cache_key) do + if content = cached_partials[cache_key] + build_rendered_template(content, template) + else yield.tap do |rendered_partial| - collection_cache.write(cache_key, rendered_partial) + collection_cache.write(cache_key, rendered_partial.body) end end end diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb index 3f3a97529d..485eb1a5b4 100644 --- a/actionview/lib/action_view/renderer/renderer.rb +++ b/actionview/lib/action_view/renderer/renderer.rb @@ -19,10 +19,14 @@ module ActionView # Main render entry point shared by Action View and Action Controller. def render(context, options) + render_to_object(context, options).body + end + + def render_to_object(context, options) # :nodoc: if options.key?(:partial) - render_partial(context, options) + render_partial_to_object(context, options) else - render_template(context, options) + render_template_to_object(context, options) end end @@ -41,16 +45,24 @@ module ActionView # Direct access to template rendering. def render_template(context, options) #:nodoc: - TemplateRenderer.new(@lookup_context).render(context, options) + render_template_to_object(context, options).body end # Direct access to partial rendering. def render_partial(context, options, &block) #:nodoc: - PartialRenderer.new(@lookup_context).render(context, options, block) + render_partial_to_object(context, options, &block).body end def cache_hits # :nodoc: @cache_hits ||= {} end + + def render_template_to_object(context, options) #:nodoc: + TemplateRenderer.new(@lookup_context).render(context, options) + end + + def render_partial_to_object(context, options, &block) #:nodoc: + PartialRenderer.new(@lookup_context).render(context, options, block) + end end end diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index f414620923..279ef3c680 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -44,7 +44,7 @@ module ActionView # object that responds to each. This object is initialized with a block # that knows how to render the template. def render_template(view, template, layout_name = nil, locals = {}) #:nodoc: - return [super] unless layout_name && template.supports_streaming? + return [super.body] unless layout_name && template.supports_streaming? locals ||= {} layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index c36baeffcd..c17d6182e8 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -10,8 +10,6 @@ module ActionView prepend_formats(template.formats) - @lookup_context.rendered_format ||= (template.formats.first || formats.first) - render_template(context, template, options[:layout], options[:locals] || {}) end @@ -46,23 +44,24 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. def render_template(view, template, layout_name, locals) - render_with_layout(view, layout_name, locals) do |layout| + render_with_layout(view, layout_name, template, locals) do |layout| instrument(:template, identifier: template.identifier, layout: layout.try(:virtual_path)) do template.render(view, locals) { |*name| view._layout_for(*name) } end end end - def render_with_layout(view, path, locals) + def render_with_layout(view, path, template, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) - if layout + body = if layout view.view_flow.set(:layout, content) layout.render(view, locals) { |*name| view._layout_for(*name) } else content end + build_rendered_template(body, template, layout) end # This is the method which actually finds the layout using details in the lookup diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index cf7d1105e0..ac861c44d4 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -26,6 +26,13 @@ module ActionView extend ActiveSupport::Concern include ActionView::ViewPaths + attr_reader :rendered_format + + def initialize + @rendered_format = nil + super + end + # Overwrite process to setup I18n proxy. def process(*) #:nodoc: old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context) @@ -82,11 +89,12 @@ module ActionView # # Override this method in a module to change the default behavior. def view_context - view_context_class.new(view_renderer, view_assigns, self) + view_context_class.new(lookup_context, view_assigns, self) end # Returns an object that is able to render templates. def view_renderer # :nodoc: + # Lifespan: Per controller @_view_renderer ||= ActionView::Renderer.new(lookup_context) end @@ -95,10 +103,6 @@ module ActionView _render_template(options) end - def rendered_format - Template::Types[lookup_context.rendered_format] - end - private # Find and render a template based on the options given. @@ -108,17 +112,22 @@ module ActionView context = view_context context.assign assigns if assigns - lookup_context.rendered_format = nil if options[:formats] lookup_context.variants = variant if variant - view_renderer.render(context, options) + rendered_template = context.in_rendering_context(options) do |renderer| + renderer.render_to_object(context, options) + end + + rendered_format = rendered_template.format || lookup_context.formats.first + @rendered_format = Template::Types[rendered_format] + + rendered_template.body end # Assign the rendered format to look up context. def _process_format(format) super lookup_context.formats = [format.to_sym] - lookup_context.rendered_format = lookup_context.formats.first end # Normalize args by converting render "foo" to render :action => "foo" and diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 37f476e554..907b322c8a 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -165,7 +165,7 @@ module ActionView def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) instrument_render_template do compile!(view) - view.run(method_name, locals, buffer, &block) + view.run(method_name, self, locals, buffer, &block) end rescue => e handle_render_error(view, e) diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb index 15ca202024..247b6d75d1 100644 --- a/actionview/lib/action_view/template/handlers/erb/erubi.rb +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -26,8 +26,8 @@ module ActionView view = Class.new(ActionView::Base) { include action_view_erb_handler_context._routes.url_helpers class_eval("define_method(:_template) { |local_assigns, output_buffer| #{src} }", @filename || "(erubi)", 0) - }.with_context(action_view_erb_handler_context) - view.run(:_template, {}, ActionView::OutputBuffer.new) + }.empty + view.run(:_template, nil, {}, ActionView::OutputBuffer.new) end private diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb index d5694d77f4..3ca5aedc14 100644 --- a/actionview/lib/action_view/view_paths.rb +++ b/actionview/lib/action_view/view_paths.rb @@ -5,13 +5,21 @@ module ActionView extend ActiveSupport::Concern included do - class_attribute :_view_paths, default: ActionView::PathSet.new.freeze + ViewPaths.set_view_paths(self, ActionView::PathSet.new.freeze) end delegate :template_exists?, :any_templates?, :view_paths, :formats, :formats=, :locale, :locale=, to: :lookup_context module ClassMethods + def _view_paths + ViewPaths.get_view_paths(self) + end + + def _view_paths=(paths) + ViewPaths.set_view_paths(self, paths) + end + def _prefixes # :nodoc: @_prefixes ||= begin return local_prefixes if superclass.abstract? @@ -29,6 +37,22 @@ module ActionView end end + # :stopdoc: + @all_view_paths = {} + + def self.get_view_paths(klass) + @all_view_paths[klass] || get_view_paths(klass.superclass) + end + + def self.set_view_paths(klass, paths) + @all_view_paths[klass] = paths + end + + def self.all_view_paths + @all_view_paths.values.uniq + end + # :startdoc: + # The prefixes used in render "foo" shortcuts. def _prefixes # :nodoc: self.class._prefixes diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 727d3fbc1a..52c3c54d96 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -174,6 +174,10 @@ class TestController < ActionController::Base render inline: "<%= controller_name %>" end + def inline_rendered_format_without_format + render inline: "test" + end + # :ported: def render_custom_code render plain: "hello world", status: 404 @@ -659,6 +663,7 @@ class RenderTest < ActionController::TestCase get :hello_world_from_rxml_using_action, to: "test#hello_world_from_rxml_using_action" get :hello_world_from_rxml_using_template, to: "test#hello_world_from_rxml_using_template" get :hello_world_with_layout_false, to: "test#hello_world_with_layout_false" + get :inline_rendered_format_without_format, to: "test#inline_rendered_format_without_format" get :layout_overriding_layout, to: "test#layout_overriding_layout" get :layout_test, to: "test#layout_test" get :layout_test_with_different_layout, to: "test#layout_test_with_different_layout" @@ -1015,6 +1020,12 @@ class RenderTest < ActionController::TestCase assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body end + def test_rendered_format_without_format + get :inline_rendered_format_without_format + assert_equal "test", @response.body + assert_equal "text/html", @response.content_type + end + def test_partials_list get :partials_list assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body diff --git a/actionview/test/activerecord/relation_cache_test.rb b/actionview/test/activerecord/relation_cache_test.rb index a6befc3ee5..6fe83dcb9a 100644 --- a/actionview/test/activerecord/relation_cache_test.rb +++ b/actionview/test/activerecord/relation_cache_test.rb @@ -11,13 +11,14 @@ class RelationCacheTest < ActionView::TestCase lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) @view_renderer = ActionView::Renderer.new(lookup_context) @virtual_path = "path" + @current_template = lookup_context.find "test/hello_world" controller.cache_store = ActiveSupport::Cache::MemoryStore.new end def test_cache_relation_other cache(Project.all) { concat("Hello World") } - assert_equal "Hello World", controller.cache_store.read("views/path/projects-#{Project.count}") + assert_equal "Hello World", controller.cache_store.read("views/test/hello_world:fa9482a68ce25bf7589b8eddad72f736/projects-#{Project.count}") end def view_cache_dependencies; []; end diff --git a/actionview/test/fixtures/test/_first.html.erb b/actionview/test/fixtures/test/_first.html.erb new file mode 100644 index 0000000000..2e2c825acb --- /dev/null +++ b/actionview/test/fixtures/test/_first.html.erb @@ -0,0 +1 @@ +"HTML" diff --git a/actionview/test/fixtures/test/_first.xml.erb b/actionview/test/fixtures/test/_first.xml.erb new file mode 100644 index 0000000000..9cf4f4ec85 --- /dev/null +++ b/actionview/test/fixtures/test/_first.xml.erb @@ -0,0 +1 @@ +"XML" diff --git a/actionview/test/fixtures/test/_first_layer.html.erb b/actionview/test/fixtures/test/_first_layer.html.erb new file mode 100644 index 0000000000..c1f1acb410 --- /dev/null +++ b/actionview/test/fixtures/test/_first_layer.html.erb @@ -0,0 +1,4 @@ +{"format":"HTML", "children": +[ + <%= render(partial: "first").chomp.html_safe %>, +]} diff --git a/actionview/test/fixtures/test/_first_layer.xml.erb b/actionview/test/fixtures/test/_first_layer.xml.erb new file mode 100644 index 0000000000..b8581bbbfc --- /dev/null +++ b/actionview/test/fixtures/test/_first_layer.xml.erb @@ -0,0 +1,4 @@ +{"format":"XML", "children": +[ + <%= render(partial: "first").chomp.html_safe %> +]} diff --git a/actionview/test/fixtures/test/_second.html.erb b/actionview/test/fixtures/test/_second.html.erb new file mode 100644 index 0000000000..2e2c825acb --- /dev/null +++ b/actionview/test/fixtures/test/_second.html.erb @@ -0,0 +1 @@ +"HTML" diff --git a/actionview/test/fixtures/test/_second.xml.erb b/actionview/test/fixtures/test/_second.xml.erb new file mode 100644 index 0000000000..9cf4f4ec85 --- /dev/null +++ b/actionview/test/fixtures/test/_second.xml.erb @@ -0,0 +1 @@ +"XML" diff --git a/actionview/test/fixtures/test/_second_layer.html.erb b/actionview/test/fixtures/test/_second_layer.html.erb new file mode 100644 index 0000000000..307706abd2 --- /dev/null +++ b/actionview/test/fixtures/test/_second_layer.html.erb @@ -0,0 +1,4 @@ +{"format":"HTML", "children": +[ + <%= render(partial: "first").chomp.html_safe %> +]} diff --git a/actionview/test/fixtures/test/_second_layer.xml.erb b/actionview/test/fixtures/test/_second_layer.xml.erb new file mode 100644 index 0000000000..b8581bbbfc --- /dev/null +++ b/actionview/test/fixtures/test/_second_layer.xml.erb @@ -0,0 +1,4 @@ +{"format":"XML", "children": +[ + <%= render(partial: "first").chomp.html_safe %> +]} diff --git a/actionview/test/fixtures/test/mixing_formats.html.erb b/actionview/test/fixtures/test/mixing_formats.html.erb new file mode 100644 index 0000000000..c65cdd7dd4 --- /dev/null +++ b/actionview/test/fixtures/test/mixing_formats.html.erb @@ -0,0 +1,5 @@ +{"format":"HTML", "children": +[ + <%= render(partial: "first", formats: :xml).chomp.html_safe %>, + <%= render(partial: "second").chomp.html_safe %> +]} diff --git a/actionview/test/fixtures/test/mixing_formats_deep.html.erb b/actionview/test/fixtures/test/mixing_formats_deep.html.erb new file mode 100644 index 0000000000..e328887eeb --- /dev/null +++ b/actionview/test/fixtures/test/mixing_formats_deep.html.erb @@ -0,0 +1,5 @@ +{"format":"HTML", "children": +[ + <%= render(partial: "first_layer", formats: :xml).chomp.html_safe %>, + <%= render(partial: "second_layer").chomp.html_safe %> +]} diff --git a/actionview/test/template/csp_helper_test.rb b/actionview/test/template/csp_helper_test.rb new file mode 100644 index 0000000000..1b7fd4665f --- /dev/null +++ b/actionview/test/template/csp_helper_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "abstract_unit" + +class CspHelperWithCspEnabledTest < ActionView::TestCase + tests ActionView::Helpers::CspHelper + + def content_security_policy_nonce + "iyhD0Yc0W+c=" + end + + def content_security_policy? + true + end + + def test_csp_meta_tag + assert_equal "<meta name=\"csp-nonce\" content=\"iyhD0Yc0W+c=\" />", csp_meta_tag + end + + def test_csp_meta_tag_with_options + assert_equal "<meta property=\"csp-nonce\" name=\"csp-nonce\" content=\"iyhD0Yc0W+c=\" />", csp_meta_tag(property: "csp-nonce") + end +end + +class CspHelperWithCspDisabledTest < ActionView::TestCase + tests ActionView::Helpers::CspHelper + + def content_security_policy? + false + end + + def test_csp_meta_tag + assert_nil csp_meta_tag + end +end diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index ddaa7febb3..91861edf11 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -7,9 +7,8 @@ require "action_view/dependency_tracker" class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = File.expand_path("../fixtures/digestor", __dir__) - def initialize(details = {}) - super(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) - @rendered_format = :html + def self.build(details = {}) + new(ActionView::PathSet.new(["digestor", "digestor/api"]), details, []) end end @@ -146,13 +145,12 @@ class TemplateDigestorTest < ActionView::TestCase end def test_nested_template_deps_with_non_default_rendered_format - finder.rendered_format = nil nested_deps = [{ "comments/comments" => ["comments/comment"] }] assert_equal nested_deps, nested_dependencies("messages/thread") end def test_template_formats_of_nested_deps_with_non_default_rendered_format - finder.rendered_format = nil + @finder = finder.with_prepended_formats([:json]) assert_equal [:json], tree_template_formats("messages/thread").uniq end @@ -161,12 +159,10 @@ class TemplateDigestorTest < ActionView::TestCase end def test_template_dependencies_with_fallback_from_js_to_html_format - finder.rendered_format = :js assert_equal ["comments/comment"], dependencies("comments/show") end def test_template_digest_with_fallback_from_js_to_html_format - finder.rendered_format = :js assert_digest_difference("comments/show") do change_template("comments/_comment") end @@ -219,14 +215,14 @@ class TemplateDigestorTest < ActionView::TestCase def test_details_are_included_in_cache_key # Cache the template digest. - @finder = FixtureFinder.new(formats: [:html]) + @finder = FixtureFinder.build(formats: [:html]) old_digest = digest("events/_event") # Change the template; the cached digest remains unchanged. change_template("events/_event") # The details are changed, so a new cache key is generated. - @finder = FixtureFinder.new + @finder = FixtureFinder.build # The cache is busted. assert_not_equal old_digest, digest("events/_event") @@ -343,9 +339,14 @@ class TemplateDigestorTest < ActionView::TestCase finder_options = options.extract!(:variants, :format) finder.variants = finder_options[:variants] || [] - finder.rendered_format = finder_options[:format] if finder_options[:format] - ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) + finder_with_formats = if finder_options[:format] + finder.with_prepended_formats(Array(finder_options[:format])) + else + finder + end + + ActionView::Digestor.digest(name: template_name, format: finder_options[:format], finder: finder_with_formats, dependencies: (options[:dependencies] || [])) end def dependencies(template_name) @@ -371,7 +372,7 @@ class TemplateDigestorTest < ActionView::TestCase end def finder - @finder ||= FixtureFinder.new + @finder ||= FixtureFinder.build end def change_template(template_name, variant = nil) diff --git a/actionview/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb index 4574b798d9..85735139c1 100644 --- a/actionview/test/template/log_subscriber_test.rb +++ b/actionview/test/template/log_subscriber_test.rb @@ -11,13 +11,12 @@ class AVLogSubscriberTest < ActiveSupport::TestCase def setup super + ActionView::LookupContext::DetailsKey.clear + view_paths = ActionController::Base.view_paths - view_paths.each(&:clear_cache) - ActionView::LookupContext.fallbacks.each(&:clear_cache) lookup_context = ActionView::LookupContext.new(view_paths, {}, ["test"]) - renderer = ActionView::Renderer.new(lookup_context) - @view = ActionView::Base.with_empty_template_cache.new(renderer, {}) + @view = ActionView::Base.with_empty_template_cache.new(lookup_context, {}) ActionView::LogSubscriber.attach_to :action_view diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index 290f832794..5298afb694 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -17,6 +17,16 @@ class LookupContextTest < ActiveSupport::TestCase I18n.locale = :en end + test "rendered_format is deprecated" do + assert_deprecated do + @lookup_context.rendered_format = "foo" + end + + assert_deprecated do + assert_equal "foo", @lookup_context.rendered_format + end + end + test "allows to override default_formats with ActionView::Base.default_formats" do formats = ActionView::Base.default_formats ActionView::Base.default_formats = [:foo, :bar] diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index b8d8717db4..ee8a110b44 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -20,7 +20,10 @@ module RenderTestCases controller = TestController.new - @controller_view = controller.view_context_class.with_empty_template_cache.new(controller.view_renderer, controller.view_assigns, controller) + @controller_view = controller.view_context_class.with_empty_template_cache.new( + controller.lookup_context, + controller.view_assigns, + controller) # Reload and register danish language for testing I18n.backend.store_translations "da", {} @@ -30,6 +33,21 @@ module RenderTestCases assert_equal ORIGINAL_LOCALES, I18n.available_locales.map(&:to_s).sort end + def test_implicit_format_comes_from_parent_template + rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats")) + assert_equal({ "format" => "HTML", + "children" => ["XML", "HTML"] }, rendered_templates) + end + + def test_implicit_format_comes_from_parent_template_cascading + rendered_templates = JSON.parse(@controller_view.render(template: "test/mixing_formats_deep")) + assert_equal({ "format" => "HTML", + "children" => [ + { "format" => "XML", "children" => ["XML"] }, + { "format" => "HTML", "children" => ["HTML"] }, + ] }, rendered_templates) + end + def test_render_without_options e = assert_raises(ArgumentError) { @view.render() } assert_match(/You invoked render but did not give any of (.+) option\./, e.message) @@ -51,11 +69,6 @@ module RenderTestCases assert_match "<error>No Comment</error>", @view.render(template: "comments/empty", formats: [:xml]) end - def test_rendered_format_without_format - @view.render(inline: "test") - assert_equal :html, @view.lookup_context.rendered_format - end - def test_render_partial_implicitly_use_format_of_the_rendered_template @view.lookup_context.formats = [:json] assert_equal "Hello world", @view.render(template: "test/one", formats: [:html]) @@ -68,7 +81,7 @@ module RenderTestCases def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names @view.lookup_context.formats = [:html] - assert_equal "\nHTML Template, but JSON partial", @view.render(template: "test/change_priority") + assert_equal "\nHTML Template, but HTML partial", @view.render(template: "test/change_priority") end def test_render_template_with_a_missing_partial_of_another_format @@ -349,6 +362,13 @@ module RenderTestCases end end + def test_without_compiled_method_container_is_deprecated + view = ActionView::Base.with_view_paths(ActionController::Base.view_paths) + assert_deprecated("ActionView::Base instances must implement `compiled_method_container`") do + assert_equal "Hello world!", view.render(file: "test/hello_world") + end + end + def test_render_partial_without_object_does_not_put_partial_name_to_local_assigns assert_equal "false", @view.render(partial: "test/partial_name_in_local_assigns") end @@ -631,9 +651,8 @@ class CachedViewRenderTest < ActiveSupport::TestCase # Ensure view path cache is primed def setup + ActionView::LookupContext::DetailsKey.clear view_paths = ActionController::Base.view_paths - view_paths.each(&:clear_cache) - ActionView::LookupContext.fallbacks.each(&:clear_cache) assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class setup_view(view_paths) end @@ -650,9 +669,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase # Test the same thing as above, but make sure the view path # is not eager loaded def setup - view_paths = ActionController::Base.view_paths - view_paths.each(&:clear_cache) - ActionView::LookupContext.fallbacks.each(&:clear_cache) + ActionView::LookupContext::DetailsKey.clear path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) view_paths = ActionView::PathSet.new([path]) assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first @@ -710,10 +727,10 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase # Ensure view path cache is primed setup do + ActionView::LookupContext::DetailsKey.clear + view_paths = ActionController::Base.view_paths assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class - view_paths.each(&:clear_cache) - ActionView::LookupContext.fallbacks.each(&:clear_cache) ActionView::PartialRenderer.collection_cache = ActiveSupport::Cache::MemoryStore.new @@ -721,10 +738,17 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase end teardown do - GC.start I18n.reload! end + test "template body written to cache" do + customer = Customer.new("david", 1) + key = cache_key(customer, "test/_customer") + assert_nil ActionView::PartialRenderer.collection_cache.read(key) + @view.render(partial: "test/customer", collection: [customer], cached: true) + assert_equal "Hello: david", ActionView::PartialRenderer.collection_cache.read(key) + end + test "collection caching does not cache by default" do customer = Customer.new("david", 1) key = cache_key(customer, "test/_customer") @@ -755,9 +779,20 @@ class CachedCollectionViewRenderTest < ActiveSupport::TestCase @view.render(partial: "test/cached_customer", collection: [customer], cached: true) end + test "collection caching does not work on multi-partials" do + a = Object.new + b = Object.new + def a.to_partial_path; "test/partial_iteration_1"; end + def b.to_partial_path; "test/partial_iteration_2"; end + + assert_raises(NotImplementedError) do + @controller_view.render(partial: [a, b], cached: true) + end + end + private def cache_key(*names, virtual_path) - digest = ActionView::Digestor.digest name: virtual_path, finder: @view.lookup_context, dependencies: [] + digest = ActionView::Digestor.digest name: virtual_path, format: :html, finder: @view.lookup_context, dependencies: [] @view.combined_fragment_cache_key([ "#{virtual_path}:#{digest}", *names ]) end end diff --git a/actionview/test/template/resolver_cache_test.rb b/actionview/test/template/resolver_cache_test.rb index 8a5db1346a..90b61a2aa1 100644 --- a/actionview/test/template/resolver_cache_test.rb +++ b/actionview/test/template/resolver_cache_test.rb @@ -4,6 +4,7 @@ require "abstract_unit" class ResolverCacheTest < ActiveSupport::TestCase def test_inspect_shields_cache_internals + ActionView::LookupContext::DetailsKey.clear assert_match %r(#<ActionView::Resolver:0x[0-9a-f]+ @cache=#<ActionView::Resolver::Cache:0x[0-9a-f]+ keys=0 queries=0>>), ActionView::Resolver.new.inspect end end diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb index 4567ee31b4..a5b59a700e 100644 --- a/actionview/test/template/streaming_render_test.rb +++ b/actionview/test/template/streaming_render_test.rb @@ -7,9 +7,9 @@ end class SetupFiberedBase < ActiveSupport::TestCase def setup + ActionView::LookupContext::DetailsKey.clear + view_paths = ActionController::Base.view_paths - view_paths.each(&:clear_cache) - ActionView::LookupContext.fallbacks.each(&:clear_cache) @assigns = { secret: "in the sauce", name: nil } @view = ActionView::Base.with_empty_template_cache.with_view_paths(view_paths, @assigns) diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index ab3ababba4..c89aff9c9d 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -24,6 +24,11 @@ module ActionView DeveloperStruct = Struct.new(:name) module SharedTests + def setup + ActionView::LookupContext::DetailsKey.clear + super + end + def self.included(test_case) test_case.class_eval do test "helpers defined on ActionView::TestCase are available" do |