diff options
Diffstat (limited to 'actionview/lib/action_view')
53 files changed, 1188 insertions, 473 deletions
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index d41fe2a608..c0d2d258c5 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -3,6 +3,7 @@ require "active_support/core_ext/module/attr_internal" require "active_support/core_ext/module/attribute_accessors" require "active_support/ordered_options" +require "active_support/deprecation" require "action_view/log_subscriber" require "action_view/helpers" require "action_view/context" @@ -179,37 +180,129 @@ module ActionView #:nodoc: def xss_safe? #:nodoc: true end + + def with_empty_template_cache # :nodoc: + subclass = Class.new(self) { + # We can't implement these as self.class because subclasses will + # share the same template cache as superclasses, so "changed?" won't work + # correctly. + define_method(:compiled_method_container) { subclass } + define_singleton_method(:compiled_method_container) { subclass } + } + end + + def changed?(other) # :nodoc: + compiled_method_container != other.compiled_method_container + end end - attr_accessor :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: @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) } end - def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc: + # :stopdoc: + + 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 + with_view_paths([]) + end + + def self.with_view_paths(view_paths, assigns = {}, controller = nil) + with_context ActionView::LookupContext.new(view_paths), assigns, controller + end + + def self.with_context(context, assigns = {}, controller = nil) + new context, assigns, controller + end + + NULL = Object.new + + # :startdoc: + + def initialize(lookup_context = nil, assigns = {}, controller = nil, formats = NULL) #:nodoc: @_config = ActiveSupport::InheritableOptions.new - if context.is_a?(ActionView::Renderer) - @view_renderer = context + unless formats == NULL + ActiveSupport::Deprecation.warn <<~eowarn + Passing formats to ActionView::Base.new is deprecated + eowarn + end + + case lookup_context + when ActionView::LookupContext + @lookup_context = lookup_context else - 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 - @view_renderer = ActionView::Renderer.new(lookup_context) + ActiveSupport::Deprecation.warn <<~eowarn + ActionView::Base instances should be constructed with a lookup context, + assigments, and a controller. + eowarn + @lookup_context = self.class.build_lookup_context(lookup_context) end + @view_renderer = ActionView::Renderer.new @lookup_context + @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 + @output_buffer = buffer + send(method, locals, buffer, &block) + ensure + @output_buffer, @virtual_path = _old_output_buffer, _old_virtual_path + end + + def compiled_method_container + 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_context(options, locals) + 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) end end diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb index 2a378fdc3c..18eaee5d79 100644 --- a/actionview/lib/action_view/buffers.rb +++ b/actionview/lib/action_view/buffers.rb @@ -3,6 +3,21 @@ require "active_support/core_ext/string/output_safety" module ActionView + # Used as a buffer for views + # + # The main difference between this and ActiveSupport::SafeBuffer + # is for the methods `<<` and `safe_expr_append=` the inputs are + # checked for nil before they are assigned and `to_s` is called on + # the input. For example: + # + # obuf = ActionView::OutputBuffer.new "hello" + # obuf << 5 + # puts obuf # => "hello5" + # + # sbuf = ActiveSupport::SafeBuffer.new "hello" + # sbuf << 5 + # puts sbuf # => "hello\u0005" + # class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc: def initialize(*) super diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index 665a9e3171..2b22c30a3a 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -1,21 +1,17 @@ # frozen_string_literal: true module ActionView - module CompiledTemplates #:nodoc: - # holds compiled template code - end - # = Action View Context # # Action View contexts are supplied to Action Controller to render a template. # The default Action View context is ActionView::Base. # - # In order to work with ActionController, a Context must just include this module. - # The initialization of the variables used by the context (@output_buffer, @view_flow, - # and @virtual_path) is responsibility of the object that includes this module - # (although you can call _prepare_context defined below). + # In order to work with Action Controller, a Context must just include this + # module. The initialization of the variables used by the context + # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the + # object that includes this module (although you can call _prepare_context + # defined below). module Context - include CompiledTemplates attr_accessor :output_buffer, :view_flow # Prepares the context by setting the appropriate instance variables. diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1cf0bd3016..6d2e471a44 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -require "concurrent/map" require "action_view/dependency_tracker" -require "monitor" module ActionView class Digestor @@ -20,9 +18,12 @@ 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: []) - dependencies ||= [] - cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + def digest(name:, finder:, dependencies: nil) + if dependencies.nil? || dependencies.empty? + cache_key = "#{name}.#{finder.rendered_format}" + else + cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join(".") + end # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@ -32,7 +33,7 @@ module ActionView root = tree(name, finder, partial) dependencies.each do |injected_dep| root.children << Injected.new(injected_dep, nil, nil) - end + end if dependencies finder.digest_cache[cache_key] = root.digest(finder) end end @@ -46,10 +47,7 @@ module ActionView def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") - options = {} - options[:formats] = [finder.rendered_format] if finder.rendered_format - - if template = finder.disable_cache { finder.find_all(logical_name, [], partial, [], options).first } + if template = find_template(finder, logical_name, [], partial, []) finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree @@ -71,6 +69,15 @@ module ActionView seen[name] ||= Missing.new(name, logical_name, nil) end end + + 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 + end + end end class Node diff --git a/actionview/lib/action_view/file_template.rb b/actionview/lib/action_view/file_template.rb new file mode 100644 index 0000000000..4921074383 --- /dev/null +++ b/actionview/lib/action_view/file_template.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require "action_view/template" + +module ActionView + class FileTemplate < Template + def initialize(filename, handler, details) + @filename = filename + + super(nil, filename, handler, details) + end + + def source + File.binread @filename + end + + def refresh(_) + self + end + + # Exceptions are marshalled when using the parallel test runner with DRb, so we need + # to ensure that references to the template object can be marshalled as well. This means forgoing + # the marshalling of the compiler mutex and instantiating that again on unmarshalling. + def marshal_dump # :nodoc: + [ @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ] + end + + def marshal_load(array) # :nodoc: + @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array + @compile_mutex = Mutex.new + end + end +end diff --git a/actionview/lib/action_view/gem_version.rb b/actionview/lib/action_view/gem_version.rb index ff7f2bb853..02de3eeec2 100644 --- a/actionview/lib/action_view/gem_version.rb +++ b/actionview/lib/action_view/gem_version.rb @@ -7,10 +7,10 @@ module ActionView end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 - PRE = "beta2" + PRE = "beta1" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 46f20c4277..0d77f74171 100644 --- a/actionview/lib/action_view/helpers.rb +++ b/actionview/lib/action_view/helpers.rb @@ -13,6 +13,7 @@ module ActionView #:nodoc: autoload :CacheHelper autoload :CaptureHelper autoload :ControllerHelper + autoload :CspHelper autoload :CsrfHelper autoload :DateHelper autoload :DebugHelper @@ -22,7 +23,6 @@ module ActionView #:nodoc: autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :OutputSafetyHelper - autoload :RecordTagHelper autoload :RenderingHelper autoload :SanitizeHelper autoload :TagHelper @@ -46,6 +46,7 @@ module ActionView #:nodoc: include CacheHelper include CaptureHelper include ControllerHelper + include CspHelper include CsrfHelper include DateHelper include DebugHelper @@ -55,7 +56,6 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include OutputSafetyHelper - include RecordTagHelper include RenderingHelper include SanitizeHelper include TagHelper diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index da630129cb..59d70a1dc4 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -47,14 +47,16 @@ module ActionView # When the last parameter is a hash you can add HTML attributes using that # parameter. The following options are supported: # - # * <tt>:extname</tt> - Append an extension to the generated url unless the extension - # already exists. This only applies for relative urls. - # * <tt>:protocol</tt> - Sets the protocol of the generated url, this option only - # applies when a relative url and +host+ options are provided. - # * <tt>:host</tt> - When a relative url is provided the host is added to the + # * <tt>:extname</tt> - Append an extension to the generated URL unless the extension + # already exists. This only applies for relative URLs. + # * <tt>:protocol</tt> - Sets the protocol of the generated URL. This option only + # applies when a relative URL and +host+ options are provided. + # * <tt>:host</tt> - When a relative URL is provided the host is added to the # that path. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline # when it is set to true. + # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if + # you have Content Security Policy enabled. # # ==== Examples # @@ -79,6 +81,9 @@ module ActionView # # javascript_include_tag "http://www.example.com/xmlhr.js" # # => <script src="http://www.example.com/xmlhr.js"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true + # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys @@ -90,7 +95,10 @@ module ActionView tag_options = { "src" => href }.merge!(options) - content_tag("script".freeze, "", tag_options) + if tag_options["nonce"] == true + tag_options["nonce"] = content_security_policy_nonce + end + content_tag("script", "", tag_options) }.join("\n").html_safe request.send_early_hints("Link" => early_hints_links.join("\n")) if respond_to?(:request) && request @@ -105,7 +113,7 @@ module ActionView # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to # apply to all media types. # - # If the server supports Early Hints header links for these assets will be + # If the server supports Early Hints header links for these assets will be # automatically pushed. # # stylesheet_link_tag "style" @@ -133,7 +141,7 @@ module ActionView sources_tags = sources.uniq.map { |source| href = path_to_stylesheet(source, path_options) - early_hints_links << "<#{href}>; rel=preload; as=stylesheet" + early_hints_links << "<#{href}>; rel=preload; as=style" tag_options = { "rel" => "stylesheet", "media" => "screen", @@ -224,8 +232,8 @@ module ActionView end # Returns a link tag that browsers can use to preload the +source+. - # The +source+ can be the path of an resource managed by asset pipeline, - # a full path or an URI. + # The +source+ can be the path of a resource managed by asset pipeline, + # a full path, or an URI. # # ==== Options # @@ -285,7 +293,7 @@ module ActionView end # Returns an HTML image tag for the +source+. The +source+ can be a full - # path, a file or an Active Storage attachment. + # path, a file, or an Active Storage attachment. # # ==== Options # @@ -321,14 +329,14 @@ module ActionView # image_tag("pic.jpg", srcset: [["pic_1024.jpg", "1024w"], ["pic_1980.jpg", "1980w"]], sizes: "100vw") # # => <img src="/assets/pic.jpg" srcset="/assets/pic_1024.jpg 1024w, /assets/pic_1980.jpg 1980w" sizes="100vw"> # - # Active Storage (images that are uploaded by the users of your app): + # Active Storage blobs (images that are uploaded by the users of your app): # # image_tag(user.avatar) # # => <img src="/rails/active_storage/blobs/.../tiger.jpg" /> - # image_tag(user.avatar.variant(resize: "100x100")) - # # => <img src="/rails/active_storage/variants/.../tiger.jpg" /> - # image_tag(user.avatar.variant(resize: "100x100"), size: '100') - # # => <img width="100" height="100" src="/rails/active_storage/variants/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100])) + # # => <img src="/rails/active_storage/representations/.../tiger.jpg" /> + # image_tag(user.avatar.variant(resize_to_limit: [100, 100]), size: '100') + # # => <img width="100" height="100" src="/rails/active_storage/representations/.../tiger.jpg" /> def image_tag(source, options = {}) options = options.symbolize_keys check_for_image_tag_errors(options) @@ -347,38 +355,16 @@ module ActionView tag("img", options) end - # Returns a string suitable for an HTML image tag alt attribute. - # The +src+ argument is meant to be an image file path. - # The method removes the basename of the file path and the digest, - # if any. It also removes hyphens and underscores from file names and - # replaces them with spaces, returning a space-separated, titleized - # string. - # - # ==== Examples - # - # image_alt('rails.png') - # # => Rails - # - # image_alt('hyphenated-file-name.png') - # # => Hyphenated file name - # - # image_alt('underscored_file_name.png') - # # => Underscored file name - def image_alt(src) - ActiveSupport::Deprecation.warn("image_alt is deprecated and will be removed from Rails 6.0. You must explicitly set alt text on images.") - - File.basename(src, ".*".freeze).sub(/-[[:xdigit:]]{32,64}\z/, "".freeze).tr("-_".freeze, " ".freeze).capitalize - end - # Returns an HTML video tag for the +sources+. If +sources+ is a string, # a single video tag will be returned. If +sources+ is an array, a video # tag with nested source tags for each source will be returned. The - # +sources+ can be full paths or files that exists in your public videos + # +sources+ can be full paths or files that exist in your public videos # directory. # # ==== Options - # You can add HTML attributes using the +options+. The +options+ supports - # two additional keys for convenience and conformance: + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. The following options are supported: # # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown # before the video loads. The path is calculated like the +src+ of +image_tag+. @@ -395,7 +381,7 @@ module ActionView # video_tag("trailer.ogg") # # => <video src="/videos/trailer.ogg"></video> # video_tag("trailer.ogg", controls: true, preload: 'none') - # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video> + # # => <video preload="none" controls="controls" src="/videos/trailer.ogg"></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png", poster_skip_pipeline: true) @@ -422,9 +408,14 @@ module ActionView end end - # Returns an HTML audio tag for the +source+. - # The +source+ can be full path or file that exists in - # your public audios directory. + # Returns an HTML audio tag for the +sources+. If +sources+ is a string, + # a single audio tag will be returned. If +sources+ is an array, an audio + # tag with nested source tags for each source will be returned. The + # +sources+ can be full paths or files that exist in your public audios + # directory. + # + # When the last parameter is a hash you can add HTML attributes using that + # parameter. # # audio_tag("sound") # # => <audio src="/audios/sound"></audio> diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb index f7690104ee..cc62783d60 100644 --- a/actionview/lib/action_view/helpers/asset_url_helper.rb +++ b/actionview/lib/action_view/helpers/asset_url_helper.rb @@ -6,7 +6,7 @@ module ActionView # = Action View Asset URL Helpers module Helpers #:nodoc: # This module provides methods for generating asset paths and - # urls. + # URLs. # # image_path("rails.png") # # => "/assets/rails.png" @@ -57,8 +57,8 @@ module ActionView # You can read more about setting up your DNS CNAME records from your ISP. # # Note: This is purely a browser performance optimization and is not meant - # for server load balancing. See http://www.die.net/musings/page_load_time/ - # for background and http://www.browserscope.org/?category=network for + # for server load balancing. See https://www.die.net/musings/page_load_time/ + # for background and https://www.browserscope.org/?category=network for # connection limit data. # # Alternatively, you can exert more control over the asset host by setting @@ -97,9 +97,10 @@ module ActionView # still sending assets for plain HTTP requests from asset hosts. If you don't # have SSL certificates for each of the asset hosts this technique allows you # to avoid warnings in the client about mixed media. - # Note that the request parameter might not be supplied, e.g. when the assets - # are precompiled via a Rake task. Make sure to use a +Proc+ instead of a lambda, - # since a +Proc+ allows missing parameters and sets them to +nil+. + # Note that the +request+ parameter might not be supplied, e.g. when the assets + # are precompiled with the command `rails assets:precompile`. Make sure to use a + # +Proc+ instead of a lambda, since a +Proc+ allows missing parameters and sets them + # to +nil+. # # config.action_controller.asset_host = Proc.new { |source, request| # if request && request.ssl? @@ -149,13 +150,13 @@ module ActionView # Below lists scenarios that apply to +asset_path+ whether or not you're # using the asset pipeline. # - # - All fully qualified urls are returned immediately. This bypasses the + # - All fully qualified URLs are returned immediately. This bypasses the # asset pipeline and all other behavior described. # # asset_path("http://www.example.com/js/xmlhr.js") # => "http://www.example.com/js/xmlhr.js" # # - All assets that begin with a forward slash are assumed to be full - # urls and will not be expanded. This will bypass the asset pipeline. + # URLs and will not be expanded. This will bypass the asset pipeline. # # asset_path("/foo.png") # => "/foo.png" # @@ -187,7 +188,7 @@ module ActionView return "" if source.blank? return source if URI_REGEXP.match?(source) - tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "".freeze) + tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, "") if extname = compute_asset_extname(source, options) source = "#{source}#{extname}" diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 3cbb1ed1a7..b1a14250c3 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -201,34 +201,42 @@ module ActionView end # This helper returns the name of a cache key for a given fragment cache - # call. By supplying +skip_digest:+ true to cache, the digestion of cache + # call. By supplying <tt>skip_digest: true</tt> to cache, the digestion of cache # fragments can be manually bypassed. This is useful when cache fragments # cannot be manually expired unless you know the exact key which is the # case when using memcached. # # The digest will be generated using +virtual_path:+ if it is provided. # - def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil) + def cache_fragment_name(name = {}, skip_digest: nil, virtual_path: nil, digest_path: nil) if skip_digest name else - fragment_name_with_digest(name, virtual_path) + fragment_name_with_digest(name, virtual_path, digest_path) + end + end + + def digest_path_from_virtual(virtual_path) # :nodoc: + digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies) + + if digest.present? + "#{virtual_path}:#{digest}" + else + virtual_path end end private - def fragment_name_with_digest(name, virtual_path) + def fragment_name_with_digest(name, virtual_path, digest_path) virtual_path ||= @virtual_path - if virtual_path + if virtual_path || digest_path name = controller.url_for(name).split("://").last if name.is_a?(Hash) - if digest = Digestor.digest(name: virtual_path, finder: lookup_context, dependencies: view_cache_dependencies).presence - [ "#{virtual_path}:#{digest}", name ] - else - [ virtual_path, name ] - end + digest_path ||= digest_path_from_virtual(virtual_path) + + [ digest_path, name ] else name end diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb index 92f7ddb70d..c87c212cc7 100644 --- a/actionview/lib/action_view/helpers/capture_helper.rb +++ b/actionview/lib/action_view/helpers/capture_helper.rb @@ -36,6 +36,10 @@ module ActionView # </body> # </html> # + # The return of capture is the string generated by the block. For Example: + # + # @greeting # => "Welcome to my shiny new web page! The date and time is 2018-09-06 11:09:16 -0500" + # def capture(*args) value = nil buffer = with_output_buffer { value = yield(*args) } diff --git a/actionview/lib/action_view/helpers/csp_helper.rb b/actionview/lib/action_view/helpers/csp_helper.rb new file mode 100644 index 0000000000..4415018845 --- /dev/null +++ b/actionview/lib/action_view/helpers/csp_helper.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionView + # = Action View CSP Helper + module Helpers #:nodoc: + module CspHelper + # Returns a meta tag "csp-nonce" with the per-session nonce value + # for allowing inline <script> tags. + # + # <head> + # <%= csp_meta_tag %> + # </head> + # + # This is used by the Rails UJS helper to create dynamically + # loaded inline <script> elements. + # + def csp_meta_tag(**options) + if content_security_policy? + options[:name] = "csp-nonce" + options[:content] = content_security_policy_nonce + tag("meta", options) + end + end + end + end +end diff --git a/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb index 69c59844a6..c0422c6ff5 100644 --- a/actionview/lib/action_view/helpers/csrf_helper.rb +++ b/actionview/lib/action_view/helpers/csrf_helper.rb @@ -20,7 +20,7 @@ module ActionView # "X-CSRF-Token" HTTP header. If you are using rails-ujs this happens automatically. # def csrf_meta_tags - if protect_against_forgery? + if defined?(protect_against_forgery?) && protect_against_forgery? [ tag("meta", name: "csrf-param", content: request_forgery_protection_token), tag("meta", name: "csrf-token", content: form_authenticity_token) diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 09040ccbc4..9d5e5eaba3 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -116,7 +116,7 @@ module ActionView when 10..19 then locale.t :less_than_x_seconds, count: 20 when 20..39 then locale.t :half_a_minute when 40..59 then locale.t :less_than_x_minutes, count: 1 - else locale.t :x_minutes, count: 1 + else locale.t :x_minutes, count: 1 end when 2...45 then locale.t :x_minutes, count: distance_in_minutes @@ -131,7 +131,7 @@ module ActionView when 43200...86400 then locale.t :about_x_months, count: (distance_in_minutes.to_f / 43200.0).round # 60 days up to 365 days when 86400...525600 then locale.t :x_months, count: (distance_in_minutes.to_f / 43200.0).round - else + else from_year = from_time.year from_year += 1 if from_time.month >= 3 to_year = to_time.year @@ -205,6 +205,7 @@ module ActionView # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to # the current selected year plus 5. + # * <tt>:year_format</tt> - Set format of years for year select. Lambda should be passed. # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the # first of the given month in order to not create invalid dates like 31 February. @@ -275,6 +276,9 @@ module ActionView # # Generates a date select with custom prompts. # date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' }) # + # # Generates a date select with custom year format. + # date_select("article", "written_on", year_format: ->(year) { "Heisei #{year - 1988}" }) + # # The selects are prepared for multi-parameter assignment to an Active Record object. # # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that @@ -302,15 +306,15 @@ module ActionView # time_select("article", "start_time", include_seconds: true) # # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45. - # time_select 'game', 'game_time', {minute_step: 15} + # time_select 'game', 'game_time', { minute_step: 15 } # # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'}) - # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' }) + # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # time_select("article", "written_on", prompt: true) # generic prompts for all # # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. - # time_select 'game', 'game_time', {ampm: true} + # time_select 'game', 'game_time', { ampm: true } # # The selects are prepared for multi-parameter assignment to an Active Record object. # @@ -346,8 +350,8 @@ module ActionView # datetime_select("article", "written_on", discard_type: true) # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # datetime_select("article", "written_on", prompt: true) # generic prompts for all # # The selects are prepared for multi-parameter assignment to an Active Record object. @@ -397,8 +401,8 @@ module ActionView # select_datetime(my_date_time, prefix: 'payday') # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours + # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours # select_datetime(my_date_time, prompt: true) # generic prompts for all def select_datetime(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_datetime @@ -436,8 +440,8 @@ module ActionView # select_date(my_date, prefix: 'payday') # # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_date(my_date, prompt: {hour: true}) # generic prompt for hours + # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_date(my_date, prompt: { hour: true }) # generic prompt for hours # select_date(my_date, prompt: true) # generic prompts for all def select_date(date = Date.current, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_date @@ -476,8 +480,8 @@ module ActionView # select_time(my_time, start_hour: 2, end_hour: 14) # # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. - # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_time(my_time, prompt: {hour: true}) # generic prompt for hours + # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_time(my_time, prompt: { hour: true }) # generic prompt for hours # select_time(my_time, prompt: true) # generic prompts for all def select_time(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_time @@ -668,8 +672,6 @@ module ActionView # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time> # time_tag Date.yesterday, 'Yesterday' # => # <time datetime="2010-11-03">Yesterday</time> - # time_tag Date.today, pubdate: true # => - # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time> # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # => # <time datetime="2010-W44">November 04, 2010</time> # @@ -681,9 +683,8 @@ module ActionView options = args.extract_options! format = options.delete(:format) || :long content = args.first || I18n.l(date_or_time, format: format) - datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 - content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block) + content_tag("time", content, options.reverse_merge(datetime: date_or_time.iso8601), &block) end private @@ -702,7 +703,7 @@ module ActionView class DateTimeSelector #:nodoc: include ActionView::Helpers::TagHelper - DEFAULT_PREFIX = "date".freeze + DEFAULT_PREFIX = "date" POSITION = { year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6 }.freeze @@ -823,7 +824,7 @@ module ActionView 1.upto(12) do |month_number| options = { value: month_number } options[:selected] = "selected" if month == month_number - month_options << content_tag("option".freeze, month_name(month_number), options) + "\n" + month_options << content_tag("option", month_name(month_number), options) + "\n" end build_select(:month, month_options.join) end @@ -851,7 +852,7 @@ module ActionView raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter." end - build_options_and_select(:year, val, options) + build_select(:year, build_year_options(val, options)) end end @@ -934,6 +935,21 @@ module ActionView end end + # Looks up year names by number. + # + # year_name(1998) # => 1998 + # + # If the <tt>:year_format</tt> option is passed: + # + # year_name(1998) # => "Heisei 10" + def year_name(number) + if year_format_lambda = @options[:year_format] + year_format_lambda.call(number) + else + number + end + end + def date_order @date_order ||= @options[:order] || translated_date_order end @@ -990,7 +1006,35 @@ module ActionView tag_options[:selected] = "selected" if selected == i text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value text = options[:ampm] ? AMPM_TRANSLATION[i] : text - select_options << content_tag("option".freeze, text, tag_options) + select_options << content_tag("option", text, tag_options) + end + + (select_options.join("\n") + "\n").html_safe + end + + # Build select option HTML for year. + # If <tt>year_format</tt> option is not passed + # build_year_options(1998, start: 1998, end: 2000) + # => "<option value="1998" selected="selected">1998</option> + # <option value="1999">1999</option> + # <option value="2000">2000</option>" + # + # If <tt>year_format</tt> option is passed + # build_year_options(1998, start: 1998, end: 2000, year_format: ->year { "Heisei #{ year - 1988 }" }) + # => "<option value="1998" selected="selected">Heisei 10</option> + # <option value="1999">Heisei 11</option> + # <option value="2000">Heisei 12</option>" + def build_year_options(selected, options = {}) + start = options.delete(:start) + stop = options.delete(:end) + step = options.delete(:step) + + select_options = [] + start.step(stop, step) do |value| + tag_options = { value: value } + tag_options[:selected] = "selected" if selected == value + text = year_name(value) + select_options << content_tag("option", text, tag_options) end (select_options.join("\n") + "\n").html_safe @@ -1009,12 +1053,12 @@ module ActionView select_options[:disabled] = "disabled" if @options[:disabled] select_options[:class] = css_class_attribute(type, select_options[:class], @options[:with_css_classes]) if @options[:with_css_classes] - select_html = "\n".dup - select_html << content_tag("option".freeze, "", value: "") + "\n" if @options[:include_blank] + select_html = +"\n" + select_html << content_tag("option", "", value: "") + "\n" if @options[:include_blank] select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt] select_html << select_options_as_html - (content_tag("select".freeze, select_html.html_safe, select_options) + "\n").html_safe + (content_tag("select", select_html.html_safe, select_options) + "\n").html_safe end # Builds the css class value for the select element @@ -1047,7 +1091,7 @@ module ActionView I18n.translate(:"datetime.prompts.#{type}", locale: @options[:locale]) end - prompt ? content_tag("option".freeze, prompt, value: "") : "" + prompt ? content_tag("option", prompt, value: "") : "" end # Builds hidden input tag for date part and value. @@ -1091,7 +1135,7 @@ module ActionView # Given an ordering of datetime components, create the selection HTML # and join them with their appropriate separators. def build_selects_from_types(order) - select = "".dup + select = +"" first_visible = order.find { |type| !@options[:"discard_#{type}"] } order.reverse_each do |type| separator = separator(type) unless type == first_visible # don't add before first visible field diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb index 52dff1f750..88ceba414b 100644 --- a/actionview/lib/action_view/helpers/debug_helper.rb +++ b/actionview/lib/action_view/helpers/debug_helper.rb @@ -24,7 +24,7 @@ module ActionView # created_at: # </pre> def debug(object) - Marshal::dump(object) + Marshal.dump(object) object = ERB::Util.html_escape(object.to_yaml) content_tag(:pre, object, class: "debug_dump") rescue # errors from Marshal or YAML diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 6185aa133f..c5a736bfb4 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -19,7 +19,7 @@ module ActionView # compared to using vanilla HTML. # # Typically, a form designed to create or update a resource reflects the - # identity of the resource in several ways: (i) the url that the form is + # identity of the resource in several ways: (i) the URL that the form is # sent to (the form element's +action+ attribute) should result in a request # being routed to the appropriate controller action (with the appropriate <tt>:id</tt> # parameter in the case of an existing resource), (ii) input fields should @@ -166,7 +166,7 @@ module ActionView # So for example you may use a named route directly. When the model is # represented by a string or symbol, as in the example above, if the # <tt>:url</tt> option is not specified, by default the form will be - # sent back to the current url (We will describe below an alternative + # sent back to the current URL (We will describe below an alternative # resource-oriented usage of +form_for+ in which the URL does not need # to be specified explicitly). # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of @@ -590,6 +590,9 @@ module ActionView # Skipped if a <tt>:url</tt> is passed. # * <tt>:scope</tt> - The scope to prefix input field names with and # thereby how the submitted parameters are grouped in controllers. + # * <tt>:namespace</tt> - A namespace for your form to ensure uniqueness of + # id attributes on form elements. The namespace attribute will be prefixed + # with underscore on the generated HTML id. # * <tt>:model</tt> - A model object to infer the <tt>:url</tt> and # <tt>:scope</tt> by, plus fill out input field values. # So if a +title+ attribute is set to "Ahoy!" then a +title+ input @@ -608,10 +611,10 @@ module ActionView # This is helpful when fragment-caching the form. Remote forms # get the authenticity token from the <tt>meta</tt> tag, so embedding is # unnecessary unless you support browsers without JavaScript. - # * <tt>:local</tt> - By default form submits are remote and unobstrusive XHRs. + # * <tt>:local</tt> - By default form submits are remote and unobtrusive XHRs. # Disable remote submits with <tt>local: true</tt>. - # * <tt>:skip_enforcing_utf8</tt> - By default a hidden field named +utf8+ - # is output to enforce UTF-8 submits. Set to true to skip the field. + # * <tt>:skip_enforcing_utf8</tt> - If set to true, a hidden input with name + # utf8 is not output. # * <tt>:builder</tt> - Override the object used to build the form. # * <tt>:id</tt> - Optional HTML id attribute. # * <tt>:class</tt> - Optional HTML class attribute. @@ -1014,14 +1017,13 @@ module ActionView # <%= fields :comment do |fields| %> # <%= fields.text_field :body %> # <% end %> - # # => <input type="text" name="comment[body]> + # # => <input type="text" name="comment[body]"> # # # Using a model infers the scope and assigns field values: - # <%= fields model: Comment.new(body: "full bodied") do |fields| %< + # <%= fields model: Comment.new(body: "full bodied") do |fields| %> # <%= fields.text_field :body %> # <% end %> - # # => - # <input type="text" name="comment[body] value="full bodied"> + # # => <input type="text" name="comment[body]" value="full bodied"> # # # Using +fields+ with +form_with+: # <%= form_with model: @post do |form| %> @@ -1128,6 +1130,9 @@ module ActionView # text_field(:post, :title, class: "create_input") # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" /> # + # text_field(:post, :title, maxlength: 30, class: "title_input") + # # => <input type="text" id="post_title" name="post[title]" maxlength="30" size="30" value="#{@post.title}" class="title_input" /> + # # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }") # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/> # @@ -1520,10 +1525,10 @@ module ActionView private def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms, - skip_enforcing_utf8: false, **options) + skip_enforcing_utf8: nil, **options) html_options = options.slice(:id, :class, :multipart, :method, :data).merge(html) html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? - html_options[:enforce_utf8] = !skip_enforcing_utf8 + html_options[:enforce_utf8] = !skip_enforcing_utf8 unless skip_enforcing_utf8.nil? html_options[:enctype] = "multipart/form-data" if html_options.delete(:multipart) @@ -1659,6 +1664,7 @@ module ActionView @nested_child_index = {} @object_name, @object, @template, @options = object_name, object, template, options @default_options = @options ? @options.slice(:index, :namespace, :skip_default_ids, :allow_method_names_outside_object) : {} + @default_html_options = @default_options.except(:skip_default_ids, :allow_method_names_outside_object) convert_to_legacy_options(@options) @@ -1674,6 +1680,227 @@ module ActionView @index = options[:index] || options[:child_index] end + ## + # :method: text_field + # + # :call-seq: text_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#text_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.text_field :name %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: password_field + # + # :call-seq: password_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#password_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.password_field :password %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: text_area + # + # :call-seq: text_area(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#text_area for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.text_area :detail %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: color_field + # + # :call-seq: color_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#color_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.color_field :favorite_color %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: search_field + # + # :call-seq: search_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#search_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.search_field :name %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: telephone_field + # + # :call-seq: telephone_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#telephone_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.telephone_field :phone %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: phone_field + # + # :call-seq: phone_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#phone_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.phone_field :phone %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: date_field + # + # :call-seq: date_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#date_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.date_field :born_on %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: time_field + # + # :call-seq: time_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#time_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.time_field :borned_at %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: datetime_field + # + # :call-seq: datetime_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#datetime_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.datetime_field :graduation_day %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: datetime_local_field + # + # :call-seq: datetime_local_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#datetime_local_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.datetime_local_field :graduation_day %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: month_field + # + # :call-seq: month_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#month_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.month_field :birthday_month %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: week_field + # + # :call-seq: week_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#week_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.week_field :birthday_week %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: url_field + # + # :call-seq: url_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#url_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.url_field :homepage %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: email_field + # + # :call-seq: email_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#email_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.email_field :address %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: number_field + # + # :call-seq: number_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#number_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.number_field :age %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + + ## + # :method: range_field + # + # :call-seq: range_field(method, options = {}) + # + # Wraps ActionView::Helpers::FormHelper#range_field for form builders: + # + # <%= form_with model: @user do |f| %> + # <%= f.range_field :age %> + # <% end %> + # + # Please refer to the documentation of the base helper for details. + (field_helpers - [:label, :check_box, :radio_button, :fields_for, :fields, :hidden_field, :file_field]).each do |selector| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 def #{selector}(method, options = {}) # def text_field(method, options = {}) @@ -1971,7 +2198,7 @@ module ActionView convert_to_legacy_options(options) - fields_for(scope || model, model, **options, &block) + fields_for(scope || model, model, options, &block) end # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object @@ -2247,7 +2474,7 @@ module ActionView @template.button_tag(value, options, &block) end - def emitted_hidden_id? + def emitted_hidden_id? # :nodoc: @emitted_hidden_id ||= nil end @@ -2267,7 +2494,12 @@ module ActionView end defaults = [] - defaults << :"helpers.submit.#{object_name}.#{key}" + # Object is a model and it is not overwritten by as and scope option. + if object.respond_to?(:model_name) && object_name.to_s == model.downcase + defaults << :"helpers.submit.#{object.model_name.i18n_key}.#{key}" + else + defaults << :"helpers.submit.#{object_name}.#{key}" + end defaults << :"helpers.submit.#{key}" defaults << "#{key.to_s.humanize} #{model}" diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 02a44477c1..a7747456a4 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -16,7 +16,7 @@ module ActionView # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, { include_blank: true }) # # could become: # @@ -30,7 +30,7 @@ module ActionView # # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' }) # # could become: # @@ -43,7 +43,7 @@ module ActionView # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' }) # # could become: # @@ -69,7 +69,7 @@ module ActionView # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' }) # # could become: # @@ -82,7 +82,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } }) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> @@ -107,7 +107,7 @@ module ActionView # # For example: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) # # would become: # @@ -214,9 +214,13 @@ module ActionView # * +method+ - The attribute of +object+ corresponding to the select tag # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags. # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an - # array of child objects representing the <tt><option></tt> tags. + # array of child objects representing the <tt><option></tt> tags. It can also be any object that responds + # to +call+, such as a +proc+, that will be called for each member of the +collection+ to retrieve the + # value. # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a - # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. + # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag. It can also be any object + # that responds to +call+, such as a +proc+, that will be called for each member of the +collection+ to + # retrieve the label. # * +option_key_method+ - The name of a method which, when called on a child object of a member of # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag. # * +option_value_method+ - The name of a method which, when called on a child object of a member of @@ -319,12 +323,12 @@ module ActionView # # You can optionally provide HTML attributes as the last element of the array. # - # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"]) + # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"]) # # => <option value="Denmark">Denmark</option> # # => <option value="USA" class="bold" selected="selected">USA</option> # # => <option value="Sweden" selected="selected">Sweden</option> # - # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]]) + # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]]) # # => <option value="$" class="bold">Dollar</option> # # => <option value="DKK" onclick="alert('HI');">Kroner</option> # @@ -457,9 +461,9 @@ module ActionView def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil) collection.map do |group| option_tags = options_from_collection_for_select( - group.send(group_method), option_key_method, option_value_method, selected_key) + value_for_collection(group, group_method), option_key_method, option_value_method, selected_key) - content_tag("optgroup".freeze, option_tags, label: group.send(group_label_method)) + content_tag("optgroup", option_tags, label: value_for_collection(group, group_label_method)) end.join.html_safe end @@ -531,7 +535,7 @@ module ActionView body = "".html_safe if prompt - body.safe_concat content_tag("option".freeze, prompt_text(prompt), value: "") + body.safe_concat content_tag("option", prompt_text(prompt), value: "") end grouped_options.each do |container| @@ -544,7 +548,7 @@ module ActionView end html_attributes = { label: label }.merge!(html_attributes) - body.safe_concat content_tag("optgroup".freeze, options_for_select(container, selected_key), html_attributes) + body.safe_concat content_tag("optgroup", options_for_select(container, selected_key), html_attributes) end body @@ -580,7 +584,7 @@ module ActionView end zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected) - zone_options.safe_concat content_tag("option".freeze, "-------------", value: "", disabled: true) + zone_options.safe_concat content_tag("option", "-------------", value: "", disabled: true) zone_options.safe_concat "\n" zones = zones - priority_zones @@ -650,7 +654,7 @@ module ActionView # # ==== Gotcha # - # The HTML specification says when nothing is select on a collection of radio buttons + # The HTML specification says when nothing is selected on a collection of radio buttons # web browsers do not send any value to server. # Unfortunately this introduces a gotcha: # if a +User+ model has a +category_id+ field and in the form no category is selected, no +category_id+ parameter is sent. So, @@ -790,7 +794,7 @@ module ActionView def extract_values_from_collection(collection, value_method, selected) if selected.is_a?(Proc) collection.map do |element| - element.send(value_method) if selected.call(element) + public_or_deprecated_send(element, value_method) if selected.call(element) end.compact else selected @@ -798,7 +802,15 @@ module ActionView end def value_for_collection(item, value) - value.respond_to?(:call) ? value.call(item) : item.send(value) + value.respond_to?(:call) ? value.call(item) : public_or_deprecated_send(item, value) + end + + def public_or_deprecated_send(item, value) + item.public_send(value) + rescue NoMethodError + raise unless item.respond_to?(value, true) && !item.respond_to?(value) + ActiveSupport::Deprecation.warn "Using private methods from view helpers is deprecated (calling private #{item.class}##{value})" + item.send(value) end def prompt_text(prompt) @@ -816,7 +828,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def select(method, choices = nil, options = {}, html_options = {}, &block) - @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block) + @template.select(@object_name, method, choices, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders: @@ -828,7 +840,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_select(method, collection, value_method, text_method, options = {}, html_options = {}) - @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options)) + @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders: @@ -840,7 +852,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {}) - @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options)) + @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders: @@ -852,7 +864,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def time_zone_select(method, priority_zones = nil, options = {}, html_options = {}) - @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options)) + @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_html_options.merge(html_options)) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders: @@ -864,7 +876,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders: @@ -876,7 +888,7 @@ module ActionView # # Please refer to the documentation of the base helper for details. def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block) - @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block) + @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_html_options.merge(html_options), &block) end end end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index e86e18dd78..c0996049f0 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -22,6 +22,8 @@ module ActionView mattr_accessor :embed_authenticity_token_in_remote_forms self.embed_authenticity_token_in_remote_forms = nil + mattr_accessor :default_enforce_utf8, default: true + # Starts a form tag that points the action to a url configured with <tt>url_for_options</tt> just like # ActionController::Base#url_for. The method for the form defaults to POST. # @@ -144,15 +146,15 @@ module ActionView end if include_blank - option_tags = content_tag("option".freeze, include_blank, options_for_blank_options_tag).safe_concat(option_tags) + option_tags = content_tag("option", include_blank, options_for_blank_options_tag).safe_concat(option_tags) end end if prompt = options.delete(:prompt) - option_tags = content_tag("option".freeze, prompt, value: "").safe_concat(option_tags) + option_tags = content_tag("option", prompt, value: "").safe_concat(option_tags) end - content_tag "select".freeze, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) + content_tag "select", option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys) end # Creates a standard text field; use these text fields to input smaller chunks of text like a username @@ -387,8 +389,8 @@ module ActionView # * Any other key creates standard HTML options for the tag. # # ==== Examples - # radio_button_tag 'gender', 'male' - # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # radio_button_tag 'favorite_color', 'maroon' + # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> # # radio_button_tag 'receive_updates', 'no', true # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> @@ -549,7 +551,8 @@ module ActionView # # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" /> def image_submit_tag(source, options = {}) options = options.stringify_keys - tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) + src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline")) + tag :input, { "type" => "image", "src" => src }.update(options) end # Creates a field set for grouping HTML form elements. @@ -574,7 +577,7 @@ module ActionView # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset> def field_set_tag(legend = nil, options = nil, &block) output = tag(:fieldset, options, true) - output.safe_concat(content_tag("legend".freeze, legend)) unless legend.blank? + output.safe_concat(content_tag("legend", legend)) unless legend.blank? output.concat(capture(&block)) if block_given? output.safe_concat("</fieldset>") end @@ -866,7 +869,7 @@ module ActionView }) end - if html_options.delete("enforce_utf8") { true } + if html_options.delete("enforce_utf8") { default_enforce_utf8 } utf8_enforcer_tag + method_tag else method_tag diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb index dd2cd57ac3..b680cb1bd3 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -15,8 +15,8 @@ module ActionView "'" => "\\'" } - JS_ESCAPE_MAP["\342\200\250".dup.force_encoding(Encoding::UTF_8).encode!] = "
" - JS_ESCAPE_MAP["\342\200\251".dup.force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP[(+"\342\200\250").force_encoding(Encoding::UTF_8).encode!] = "
" + JS_ESCAPE_MAP[(+"\342\200\251").force_encoding(Encoding::UTF_8).encode!] = "
" # Escapes carriage returns and single and double quotes for JavaScript segments. # @@ -25,12 +25,13 @@ module ActionView # # $('some_element').replaceWith('<%= j render 'some/element_template' %>'); def escape_javascript(javascript) - if javascript - result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] } - javascript.html_safe? ? result.html_safe : result + javascript = javascript.to_s + if javascript.empty? + result = "" else - "" + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) { |match| JS_ESCAPE_MAP[match] } end + javascript.html_safe? ? result.html_safe : result end alias_method :j, :escape_javascript @@ -63,6 +64,13 @@ module ActionView # <%= javascript_tag defer: 'defer' do -%> # alert('All is good') # <% end -%> + # + # If you have a content security policy enabled then you can add an automatic + # nonce value by passing <tt>nonce: true</tt> as part of +html_options+. Example: + # + # <%= javascript_tag nonce: true do -%> + # alert('All is good') + # <% end -%> def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block) content = if block_given? @@ -72,7 +80,11 @@ module ActionView content_or_options_with_block end - content_tag("script".freeze, javascript_cdata_section(content), html_options) + if html_options[:nonce] == true + html_options[:nonce] = content_security_policy_nonce + end + + content_tag("script", javascript_cdata_section(content), html_options) end def javascript_cdata_section(content) #:nodoc: diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb index 4b53b8fe6e..35206b7e48 100644 --- a/actionview/lib/action_view/helpers/number_helper.rb +++ b/actionview/lib/action_view/helpers/number_helper.rb @@ -100,6 +100,9 @@ module ActionView # absolute value of the number. # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when # the argument is invalid. + # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes + # insignificant zeros after the decimal separator (defaults to + # +false+). # # ==== Examples # @@ -117,6 +120,8 @@ module ActionView # # => R$1234567890,50 # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u") # # => 1234567890,50 R$ + # number_to_currency(1234567890.50, strip_insignificant_zeros: true) + # # => "$1,234,567,890.5" def number_to_currency(number, options = {}) delegate_number_helper_method(:number_to_currency, number, options) end diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb deleted file mode 100644 index a6953ee905..0000000000 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module ActionView - module Helpers #:nodoc: - module RecordTagHelper - def div_for(*) # :nodoc: - raise NoMethodError, "The `div_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - - def content_tag_for(*) # :nodoc: - raise NoMethodError, "The `content_tag_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - end - end -end diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb index 8e505ab054..7323963c72 100644 --- a/actionview/lib/action_view/helpers/rendering_helper.rb +++ b/actionview/lib/action_view/helpers/rendering_helper.rb @@ -13,7 +13,6 @@ module ActionView # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>. # * <tt>:file</tt> - Renders an explicit template file (this used to be the old default), add :locals to pass in those. # * <tt>:inline</tt> - Renders an inline template similar to how it's done in the controller. - # * <tt>:text</tt> - Renders the text passed in out. # * <tt>:plain</tt> - Renders the text passed in out. Setting the content # type as <tt>text/plain</tt>. # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise @@ -28,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_context(options, locals) 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/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index 275a2dffb4..f4fa133f55 100644 --- a/actionview/lib/action_view/helpers/sanitize_helper.rb +++ b/actionview/lib/action_view/helpers/sanitize_helper.rb @@ -10,7 +10,7 @@ module ActionView # These helper methods extend Action View making them callable within your template files. module SanitizeHelper extend ActiveSupport::Concern - # Sanitizes HTML input, stripping all tags and attributes that aren't whitelisted. + # Sanitizes HTML input, stripping all but known-safe tags and attributes. # # It also strips href/src attributes with unsafe protocols like # <tt>javascript:</tt>, while also protecting against attempts to use Unicode, @@ -40,7 +40,7 @@ module ActionView # # <%= sanitize @comment.body %> # - # Providing custom whitelisted tags and attributes: + # Providing custom lists of permitted tags and attributes: # # <%= sanitize @comment.body, tags: %w(strong em a), attributes: %w(href) %> # @@ -126,7 +126,7 @@ module ActionView attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer # Vendors the full, link and white list sanitizers. - # Provided strictly for compatibility and can be removed in Rails 5.1. + # Provided strictly for compatibility and can be removed in Rails 6. def sanitizer_vendor Rails::Html::Sanitizer end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a6cec3f69c..3979721d34 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -58,7 +58,7 @@ module ActionView def tag_options(options, escape = true) return if options.blank? - output = "".dup + output = +"" sep = " " options.each_pair do |key, value| if TAG_PREFIXES.include?(key) && value.is_a?(Hash) @@ -86,11 +86,12 @@ module ActionView def tag_option(key, value, escape) if value.is_a?(Array) - value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) + value = escape ? safe_join(value, " ") : value.join(" ") else - value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s + value = escape ? ERB::Util.unwrapped_html_escape(value) : value.to_s.dup end - %(#{key}="#{value.gsub('"'.freeze, '"'.freeze)}") + value.gsub!('"', """) + %(#{key}="#{value}") end private @@ -227,10 +228,10 @@ module ActionView # tag("img", src: "open & shut.png") # # => <img src="open & shut.png" /> # - # tag("img", {src: "open & shut.png"}, false, false) + # tag("img", { src: "open & shut.png" }, false, false) # # => <img src="open & shut.png" /> # - # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) + # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) }) # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> def tag(name = nil, options = nil, open = false, escape = true) if name.nil? diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index fed908fcdb..0adecf362a 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -109,11 +109,11 @@ module ActionView # a little duplication to construct less strings case when @object_name.empty? - "#{sanitized_method_name}#{"[]" if multiple}" + "#{sanitized_method_name}#{multiple ? "[]" : ""}" when index - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" + "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}" else - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" + "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}" end end @@ -138,7 +138,7 @@ module ActionView end def sanitized_value(value) - value.to_s.gsub(/\s/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s + value.to_s.gsub(/[\s\.]/, "_").gsub(/[^-[[:word:]]]/, "").mb_chars.downcase.to_s end def select_content_tag(option_tags, options, html_options) @@ -170,7 +170,11 @@ module ActionView option_tags = tag_builder.content_tag_string("option", options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, value: "") + "\n" + option_tags end if value.blank? && options[:prompt] - option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), value: "") + "\n" + option_tags + tag_options = { value: "" }.tap do |prompt_opts| + prompt_opts[:disabled] = true if options[:disabled] == "" + prompt_opts[:selected] = true if options[:selected] == "" + end + option_tags = tag_builder.content_tag_string("option", prompt_text(options[:prompt]), tag_options) + "\n" + option_tags end option_tags end diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb index c5f0bb6bbb..39ab1285c3 100644 --- a/actionview/lib/action_view/helpers/tags/color_field.rb +++ b/actionview/lib/action_view/helpers/tags/color_field.rb @@ -15,7 +15,7 @@ module ActionView def validate_color_string(string) regex = /#[0-9a-fA-F]{6}/ - if regex.match(string) + if regex.match?(string) string.downcase else "#000000" diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb index 345484ba92..790721a0b7 100644 --- a/actionview/lib/action_view/helpers/tags/select.rb +++ b/actionview/lib/action_view/helpers/tags/select.rb @@ -8,7 +8,7 @@ module ActionView @choices = block_given? ? template_object.capture { yield || "" } : choices @choices = @choices.to_a if @choices.is_a?(Range) - @html_options = html_options.except(:skip_default_ids, :allow_method_names_outside_object) + @html_options = html_options super(object_name, method_name, template_object, options) end diff --git a/actionview/lib/action_view/helpers/tags/translator.rb b/actionview/lib/action_view/helpers/tags/translator.rb index fcf96d2c9c..e81ca3aef0 100644 --- a/actionview/lib/action_view/helpers/tags/translator.rb +++ b/actionview/lib/action_view/helpers/tags/translator.rb @@ -16,13 +16,8 @@ module ActionView translated_attribute || human_attribute_name end - # TODO Change this to private once we've dropped Ruby 2.2 support. - # Workaround for Ruby 2.2 "private attribute?" warning. - protected - - attr_reader :object_name, :method_and_value, :scope, :model - private + attr_reader :object_name, :method_and_value, :scope, :model def i18n_default if model diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 84d38aa416..c282505e13 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -13,9 +13,9 @@ module ActionView # # ==== Sanitization # - # Most text helpers by default sanitize the given content, but do not escape it. - # This means HTML tags will appear in the page but all malicious code will be removed. - # Let's look at some examples using the +simple_format+ method: + # Most text helpers that generate HTML output sanitize the given input by default, + # but do not escape it. This means HTML tags will appear in the page but all malicious + # code will be removed. Let's look at some examples using the +simple_format+ method: # # simple_format('<a href="http://example.com/">Example</a>') # # => "<p><a href=\"http://example.com/\">Example</a></p>" @@ -128,7 +128,7 @@ module ActionView # # => You searched for: <a href="search?q=rails">rails</a> # # highlight('<a href="javascript:alert(\'no!\')">ruby</a> on rails', 'rails', sanitize: false) - # # => "<a>ruby</a> on <mark>rails</mark>" + # # => <a href="javascript:alert('no!')">ruby</a> on <mark>rails</mark> def highlight(text, phrases, options = {}) text = sanitize(text) if options.fetch(:sanitize, true) @@ -188,7 +188,7 @@ module ActionView unless separator.empty? text.split(separator).each do |value| - if value.match(regex) + if value.match?(regex) phrase = value break end @@ -228,7 +228,7 @@ module ActionView # pluralize(2, 'Person', locale: :de) # # => 2 Personen def pluralize(count, singular, plural_arg = nil, plural: plural_arg, locale: I18n.locale) - word = if (count == 1 || count =~ /^1(\.0+)?$/) + word = if count == 1 || count.to_s =~ /^1(\.0+)?$/ singular else plural || singular.pluralize(locale) @@ -259,7 +259,7 @@ module ActionView # # => Once\r\nupon\r\na\r\ntime def word_wrap(text, line_width: 80, break_sequence: "\n") text.split("\n").collect! do |line| - line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").strip : line + line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1#{break_sequence}").rstrip : line end * break_sequence end diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 1860bc4732..67c86592b9 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -59,11 +59,9 @@ module ActionView # they can provide HTML values for. def translate(key, options = {}) options = options.dup - has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)).compact - - if has_default && !remaining_defaults.first.kind_of?(Symbol) - options[:default] = remaining_defaults + if options.has_key?(:default) + remaining_defaults = Array(options.delete(:default)).compact + options[:default] = remaining_defaults unless remaining_defaults.first.kind_of?(Symbol) end # If the user has explicitly decided to NOT raise errors, pass that option to I18n. @@ -85,8 +83,11 @@ module ActionView end end translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) - - translation.respond_to?(:html_safe) ? translation.html_safe : translation + if translation.respond_to?(:map) + translation.map { |element| element.respond_to?(:html_safe) ? element.html_safe : element } + else + translation.respond_to?(:html_safe) ? translation.html_safe : translation + end else I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end @@ -97,7 +98,7 @@ module ActionView raise e if raise_error keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope]) - title = "translation missing: #{keys.join('.')}".dup + title = +"translation missing: #{keys.join('.')}" interpolations = options.except(:default, :scope) if interpolations.any? @@ -122,9 +123,12 @@ module ActionView private def scope_key_by_partial(key) - if key.to_s.first == "." + stringified_key = key.to_s + if stringified_key.first == "." if @virtual_path - @virtual_path.gsub(%r{/_?}, ".") + key.to_s + @_scope_key_by_partial_cache ||= {} + @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".") + "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}" else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end @@ -134,7 +138,7 @@ module ActionView end def html_safe_translation_key?(key) - /(\b|_|\.)html$/.match?(key.to_s) + /([_.]|\b)html\z/.match?(key.to_s) end end end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 889562c478..d63ada3890 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -200,9 +200,9 @@ module ActionView html_options = convert_options_to_data_attributes(options, html_options) url = url_for(options) - html_options["href".freeze] ||= url + html_options["href"] ||= url - content_tag("a".freeze, name || url, html_options, &block) + content_tag("a", name || url, html_options, &block) end # Generates a form containing a single button that submits to the URL created @@ -308,7 +308,7 @@ module ActionView params = html_options.delete("params") method = html_options.delete("method").to_s - method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".freeze.html_safe + method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : "".html_safe form_method = method == "get" ? "get" : "post" form_options = html_options.delete("form") || {} @@ -321,7 +321,7 @@ module ActionView request_method = method.empty? ? "post" : method token_tag(nil, form_options: { action: url, method: request_method }) else - "".freeze + "" end html_options = convert_options_to_data_attributes(options, html_options) @@ -487,12 +487,12 @@ module ActionView option = html_options.delete(item).presence || next "#{item.dasherize}=#{ERB::Util.url_encode(option)}" }.compact - extras = extras.empty? ? "".freeze : "?" + extras.join("&") + extras = extras.empty? ? "" : "?" + extras.join("&") encoded_email_address = ERB::Util.url_encode(email_address).gsub("%40", "@") html_options["href"] = "mailto:#{encoded_email_address}#{extras}" - content_tag("a".freeze, name || email_address, html_options, &block) + content_tag("a", name || email_address, html_options, &block) end # True if the current request URI was generated by the given +options+. @@ -575,21 +575,21 @@ module ActionView def convert_options_to_data_attributes(options, html_options) if html_options html_options = html_options.stringify_keys - html_options["data-remote"] = "true".freeze if link_to_remote_options?(options) || link_to_remote_options?(html_options) + html_options["data-remote"] = "true" if link_to_remote_options?(options) || link_to_remote_options?(html_options) - method = html_options.delete("method".freeze) + method = html_options.delete("method") add_method_to_attributes!(html_options, method) if method html_options else - link_to_remote_options?(options) ? { "data-remote" => "true".freeze } : {} + link_to_remote_options?(options) ? { "data-remote" => "true" } : {} end end def link_to_remote_options?(options) if options.is_a?(Hash) - options.delete("remote".freeze) || options.delete(:remote) + options.delete("remote") || options.delete(:remote) end end @@ -618,11 +618,11 @@ module ActionView end def token_tag(token = nil, form_options: {}) - if token != false && protect_against_forgery? + if token != false && defined?(protect_against_forgery?) && protect_against_forgery? token ||= form_authenticity_token(form_options: form_options) tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token) else - "".freeze + "" end end @@ -634,9 +634,9 @@ module ActionView # suitable for use as the names and values of form input fields: # # to_form_params(name: 'David', nationality: 'Danish') - # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] + # # => [{name: 'name', value: 'David'}, {name: 'nationality', value: 'Danish'}] # - # to_form_params(country: {name: 'Denmark'}) + # to_form_params(country: { name: 'Denmark' }) # # => [{name: 'country[name]', value: 'Denmark'}] # # to_form_params(countries: ['Denmark', 'Sweden']}) @@ -666,7 +666,7 @@ module ActionView params.push(*to_form_params(value, array_prefix)) end else - params << { name: namespace, value: attribute.to_param } + params << { name: namespace.to_s, value: attribute.to_param } end params.sort_by { |pair| pair[:name] } diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb index d4ac77e10f..227f025385 100644 --- a/actionview/lib/action_view/log_subscriber.rb +++ b/actionview/lib/action_view/log_subscriber.rb @@ -16,17 +16,17 @@ module ActionView def render_template(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end def render_partial(event) info do - message = " Rendered #{from_rails_root(event.payload[:identifier])}".dup + message = +" Rendered #{from_rails_root(event.payload[:identifier])}" message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout] - message << " (#{event.duration.round(1)}ms)" + message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil? message end @@ -37,7 +37,7 @@ module ActionView info do " Rendered collection of #{from_rails_root(identifier)}" \ - " #{render_count(event.payload)} (#{event.duration.round(1)}ms)" + " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})" end end @@ -85,7 +85,7 @@ module ActionView def log_rendering_start(payload) info do - message = " Rendering #{from_rails_root(payload[:identifier])}".dup + message = +" Rendering #{from_rails_root(payload[:identifier])}" message << " within #{from_rails_root(payload[:layout])}" if payload[:layout] message end diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index 0e56eca35c..125ab4dbe3 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -3,6 +3,7 @@ require "concurrent/map" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/module/attribute_accessors" +require "active_support/deprecation" require "action_view/template/resolver" module ActionView @@ -24,7 +25,7 @@ module ActionView registered_details << name Accessors::DEFAULT_PROCS[name] = block - Accessors.send :define_method, :"default_#{name}", &block + Accessors.define_method(:"default_#{name}", &block) Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name} @details.fetch(:#{name}, []) @@ -57,21 +58,36 @@ 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) + @view_context_class ||= klass.with_empty_template_cache end end @@ -82,7 +98,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. @@ -96,7 +112,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 @@ -106,12 +123,6 @@ module ActionView module ViewPaths attr_reader :view_paths, :html_fallback_for_js - # Whenever setting view paths, makes a copy so that we can manipulate them in - # instance objects as we wish. - def view_paths=(paths) - @view_paths = ActionView::PathSet.new(Array(paths)) - end - def find(name, prefixes = [], partial = false, keys = [], options = {}) @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options)) end @@ -138,19 +149,34 @@ module ActionView # Adds fallbacks to the view paths. Useful in cases when you are rendering # a :file. def with_fallbacks - added_resolvers = 0 - self.class.fallbacks.each do |resolver| - next if view_paths.include?(resolver) - view_paths.push(resolver) - added_resolvers += 1 + view_paths = build_view_paths((@view_paths.paths + self.class.fallbacks).uniq) + + if block_given? + ActiveSupport::Deprecation.warn <<~eowarn + Calling `with_fallbacks` with a block is deprecated. Call methods on + the lookup context returned by `with_fallbacks` instead. + eowarn + + begin + _view_paths = @view_paths + @view_paths = view_paths + yield + ensure + @view_paths = _view_paths + end + else + ActionView::LookupContext.new(view_paths, @details, @prefixes) end - yield - ensure - added_resolvers.times { view_paths.pop } end private + # Whenever setting view paths, makes a copy so that we can manipulate them in + # instance objects as we wish. + def build_view_paths(paths) + ActionView::PathSet.new(Array(paths)) + end + def args_for_lookup(name, prefixes, partial, keys, details_options) name, prefixes = normalize_name(name, prefixes) details, details_key = detail_args_for(details_options) @@ -163,7 +189,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 @@ -190,7 +216,7 @@ module ActionView end if @cache - [details, DetailsKey.get(details)] + [details, DetailsKey.details_cache_key(details)] else [details, nil] end @@ -202,13 +228,13 @@ module ActionView # name instead of the prefix. def normalize_name(name, prefixes) prefixes = prefixes.presence - parts = name.to_s.split("/".freeze) + parts = name.to_s.split("/") parts.shift if parts.first.empty? name = parts.pop return name, prefixes || [""] if parts.empty? - parts = parts.join("/".freeze) + parts = parts.join("/") prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts] return name, prefixes @@ -221,16 +247,24 @@ 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) - self.view_paths = view_paths + @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) @@ -245,7 +279,7 @@ module ActionView # add :html as fallback to :js. def formats=(values) if values - values.concat(default_formats) if values.delete "*/*".freeze + values.concat(default_formats) if values.delete "*/*" if values == [:js] values << :html @html_fallback_for_js = true diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 73dfb267bb..a25e1d3d02 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -6,9 +6,13 @@ require "rails" module ActionView # = Action View Railtie class Railtie < Rails::Engine # :nodoc: + NULL_OPTION = Object.new + config.action_view = ActiveSupport::OrderedOptions.new config.action_view.embed_authenticity_token_in_remote_forms = nil config.action_view.debug_missing_translation = true + config.action_view.default_enforce_utf8 = nil + config.action_view.finalize_compiled_template_methods = NULL_OPTION config.eager_load_namespaces << ActionView @@ -35,6 +39,25 @@ module ActionView end end + initializer "action_view.default_enforce_utf8" do |app| + ActiveSupport.on_load(:action_view) do + default_enforce_utf8 = app.config.action_view.delete(:default_enforce_utf8) + unless default_enforce_utf8.nil? + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = default_enforce_utf8 + end + end + end + + initializer "action_view.finalize_compiled_template_methods" do |app| + ActiveSupport.on_load(:action_view) do + option = app.config.action_view.delete(:finalize_compiled_template_methods) + + if option != NULL_OPTION + ActiveSupport::Deprecation.warn "action_view.finalize_compiled_template_methods is deprecated and has no effect" + end + end + end + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb index 1310a1ce0a..ee39b6050d 100644 --- a/actionview/lib/action_view/record_identifier.rb +++ b/actionview/lib/action_view/record_identifier.rb @@ -59,8 +59,8 @@ module ActionView include ModelNaming - JOIN = "_".freeze - NEW = "new".freeze + JOIN = "_" + NEW = "new" # The DOM class convention is to use the singular form of an object or class. # diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb index 20b2523cac..ae366ce54a 100644 --- a/actionview/lib/action_view/renderer/abstract_renderer.rb +++ b/actionview/lib/action_view/renderer/abstract_renderer.rb @@ -17,7 +17,7 @@ module ActionView # that new object is called in turn. This abstracts the setup and rendering # into a separate classes for partials and templates. class AbstractRenderer #:nodoc: - delegate :find_template, :find_file, :template_exists?, :any_templates?, :with_fallbacks, :with_layout_format, :formats, to: :@lookup_context + delegate :template_exists?, :any_templates?, :formats, to: :@lookup_context def initialize(lookup_context) @lookup_context = lookup_context @@ -38,8 +38,6 @@ module ActionView end def instrument(name, **options) # :doc: - options[:identifier] ||= (@template && @template.identifier) || @path - ActiveSupport::Notifications.instrument("render_#{name}.action_view", options) do |payload| yield payload end diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb index 5b40af4f2f..f8a6f13ae9 100644 --- a/actionview/lib/action_view/renderer/partial_renderer.rb +++ b/actionview/lib/action_view/renderer/partial_renderer.rb @@ -295,43 +295,65 @@ module ActionView end def render(context, options, block) - setup(context, options, block) - @template = find_partial + as = as_variable(options) + setup(context, options, as, block) + + if @path + if @has_object || @collection + @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) + @template_keys = retrieve_template_keys(@variable) + else + @template_keys = @locals.keys + end + template = find_partial(@path, @template_keys) + @variable ||= template.variable + else + if options[:cached] + raise NotImplementedError, "render caching requires a template. Please specify a partial when rendering" + end + template = nil + end @lookup_context.rendered_format ||= begin - if @template && @template.formats.present? - @template.formats.first + if template && template.formats.first + template.formats.first else formats.first end end if @collection - render_collection + render_collection(context, template) else - render_partial + render_partial(context, template) end end private - def render_collection - instrument(:collection, count: @collection.size) do |payload| + def render_collection(view, template) + identifier = (template && template.identifier) || @path + instrument(:collection, identifier: identifier, count: @collection.size) do |payload| return nil if @collection.blank? if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + spacer = find_template(@options[:spacer_template], @locals.keys).render(view, @locals) end - cache_collection_render(payload) do - @template ? collection_with_template : collection_without_template - 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 + collection_body.join(spacer).html_safe end end - def render_partial - instrument(:partial) do |payload| - view, locals, block = @view, @locals, @block + def render_partial(view, template) + instrument(:partial, identifier: template.identifier) do |payload| + locals, block = @locals, @block object, as = @object, @variable if !block && (layout = @options[:layout]) @@ -341,12 +363,12 @@ module ActionView object = locals[as] if object.nil? # Respect object when object is false locals[as] = object if @has_object - content = @template.render(view, locals) do |*name| + content = template.render(view, locals) do |*name| view._layout_for(*name, &block) end content = layout.render(view, locals) { content } if layout - payload[:cache_hit] = view.view_renderer.cache_hits[@template.virtual_path] + payload[:cache_hit] = view.view_renderer.cache_hits[template.virtual_path] content end end @@ -358,16 +380,13 @@ module ActionView # If +options[:partial]+ is a string, then the <tt>@path</tt> instance variable is # set to that string. Otherwise, the +options[:partial]+ object must # respond to +to_partial_path+ in order to setup the path. - def setup(context, options, block) - @view = context + def setup(context, options, as, block) @options = options @block = block @locals = options[:locals] || {} @details = extract_details(options) - prepend_formats(options[:formats]) - partial = options[:partial] if String === partial @@ -381,26 +400,26 @@ module ActionView @collection = collection_from_object || collection_from_options if @collection - paths = @collection_data = @collection.map { |o| partial_path(o) } - @path = paths.uniq.one? ? paths.first : nil + paths = @collection_data = @collection.map { |o| partial_path(o, context) } + if paths.uniq.length == 1 + @path = paths.first + else + paths.map! { |path| retrieve_variable(path, as).unshift(path) } + @path = nil + end else - @path = partial_path + @path = partial_path(@object, context) end end + self + end + + def as_variable(options) if as = options[:as] raise_invalid_option_as(as) unless /\A[a-z_]\w*\z/.match?(as.to_s) - as = as.to_sym + as.to_sym end - - if @path - @variable, @variable_counter, @variable_iteration = retrieve_variable(@path, as) - @template_keys = retrieve_template_keys - else - paths.map! { |path| retrieve_variable(path, as).unshift(path) } - end - - self end def collection_from_options @@ -414,8 +433,8 @@ module ActionView @object.to_ary if @object.respond_to?(:to_ary) end - def find_partial - find_template(@path, @template_keys) if @path + def find_partial(path, template_keys) + find_template(path, template_keys) end def find_template(path, locals) @@ -423,8 +442,8 @@ module ActionView @lookup_context.find_template(path, prefixes, true, locals, @details) end - def collection_with_template - view, locals, template = @view, @locals, @template + def collection_with_template(view, template) + locals = @locals as, counter, iteration = @variable, @variable_counter, @variable_iteration if layout = @options[:layout] @@ -445,8 +464,8 @@ module ActionView end end - def collection_without_template - view, locals, collection_data = @view, @locals, @collection_data + def collection_without_template(view) + locals, collection_data = @locals, @collection_data cache = {} keys = @locals.keys @@ -474,7 +493,7 @@ module ActionView # # If +prefix_partial_path_with_controller_namespace+ is true, then this # method will prefix the partial paths with a namespace. - def partial_path(object = @object) + def partial_path(object, view) object = object.to_model if object.respond_to?(:to_model) path = if object.respond_to?(:to_partial_path) @@ -483,7 +502,7 @@ module ActionView raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.") end - if @view.prefix_partial_path_with_controller_namespace + if view.prefix_partial_path_with_controller_namespace prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup) else path @@ -511,9 +530,9 @@ module ActionView end end - def retrieve_template_keys + def retrieve_template_keys(variable) keys = @locals.keys - keys << @variable if @has_object || @collection + keys << variable if @collection keys << @variable_counter keys << @variable_iteration @@ -523,7 +542,7 @@ module ActionView def retrieve_variable(path, as) variable = as || begin - base = path[-1] == "/".freeze ? "".freeze : File.basename(path) + base = path[-1] == "/" ? "" : File.basename(path) raise_invalid_identifier(path) unless base =~ /\A_?(.*?)(?:\.\w+)*\z/ $1.to_sym 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 db52919e91..388f9e5e56 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -11,18 +11,38 @@ module ActionView end private - def cache_collection_render(instrumentation_payload) + def cache_collection_render(instrumentation_payload, view, template) return yield unless @options[:cached] - keyed_collection = collection_by_cache_keys + # Result is a hash with the key represents the + # key used for cache lookup and the value is the item + # on which the partial is being rendered + keyed_collection = collection_by_cache_keys(view, template) + + # Pull all partials from cache + # Result is a hash, key matches the entry in + # `keyed_collection` where the cache was retrieved and the + # value is the value that was present in the cache cached_partials = collection_cache.read_multi(*keyed_collection.keys) instrumentation_payload[:cache_hits] = cached_partials.size + # Extract the items for the keys that are not found + # Set the uncached values to instance variable @collection + # which is used by the caller @collection = keyed_collection.reject { |key, _| cached_partials.key?(key) }.values + + # If all elements are already in cache then + # rendered partials will be an empty array + # + # If the cache is missing elements then + # the block will be called against the remaining items + # in the @collection. rendered_partials = @collection.empty? ? [] : yield index = 0 fetch_or_cache_partial(cached_partials, 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 } end end @@ -31,19 +51,36 @@ module ActionView @options[:cached].respond_to?(:call) end - def collection_by_cache_keys + 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) + @collection.each_with_object({}) do |item, hash| - hash[expanded_cache_key(seed.call(item))] = item + hash[expanded_cache_key(seed.call(item), view, template, digest_path)] = item end end - def expanded_cache_key(key) - key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_path)) + def expanded_cache_key(key, view, template, digest_path) + key = view.combined_fragment_cache_key(view.cache_fragment_name(key, virtual_path: template.virtual_path, digest_path: digest_path)) key.frozen? ? key.dup : key # #read_multi & #write may require mutability, Dalli 2.6.0. end + # `order_by` is an enumerable object containing keys of the cache, + # all keys are passed in whether found already or not. + # + # `cached_partials` is a hash. If the value exists + # it represents the rendered partial from the cache + # otherwise `Hash#fetch` will take the value of its block. + # + # This method expects a block that will return the rendered + # partial. An example is to render all results + # for each element that was not found in the cache and store it as an array. + # Order it so that the first empty cache element in `cached_partials` + # corresponds to the first element in `rendered_partials`. + # + # 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:) order_by.map do |cache_key| cached_partials.fetch(cache_key) do diff --git a/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb index 276a28ce07..f414620923 100644 --- a/actionview/lib/action_view/renderer/streaming_template_renderer.rb +++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb @@ -33,7 +33,7 @@ module ActionView logger = ActionView::Base.logger return unless logger - message = "\n#{exception.class} (#{exception.message}):\n".dup + message = +"\n#{exception.class} (#{exception.message}):\n" message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code) message << " " << exception.backtrace.join("\n ") logger.fatal("#{message}\n\n") @@ -43,14 +43,14 @@ module ActionView # For streaming, instead of rendering a given a template, we return a Body # object that responds to each. This object is initialized with a block # that knows how to render the template. - def render_template(template, layout_name = nil, locals = {}) #:nodoc: + def render_template(view, template, layout_name = nil, locals = {}) #:nodoc: return [super] unless layout_name && template.supports_streaming? locals ||= {} layout = layout_name && find_layout(layout_name, locals.keys, [formats.first]) Body.new do |buffer| - delayed_render(buffer, template, layout, @view, locals) + delayed_render(buffer, template, layout, view, locals) end end diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb index ce8908924a..c36baeffcd 100644 --- a/actionview/lib/action_view/renderer/template_renderer.rb +++ b/actionview/lib/action_view/renderer/template_renderer.rb @@ -5,7 +5,6 @@ require "active_support/core_ext/object/try" module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) - @view = context @details = extract_details(options) template = determine_template(options) @@ -13,7 +12,7 @@ module ActionView @lookup_context.rendered_format ||= (template.formats.first || formats.first) - render_template(template, options[:layout], options[:locals]) + render_template(context, template, options[:layout], options[:locals] || {}) end private @@ -29,7 +28,7 @@ module ActionView elsif options.key?(:html) Template::HTML.new(options[:html], formats.first) elsif options.key?(:file) - with_fallbacks { find_file(options[:file], nil, false, keys, @details) } + @lookup_context.with_fallbacks.find_file(options[:file], nil, false, keys, @details) elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, locals: keys) @@ -37,7 +36,7 @@ module ActionView if options[:template].respond_to?(:render) options[:template] else - find_template(options[:template], options[:prefixes], false, keys, @details) + @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details) end else raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file, :plain, :html or :body option." @@ -46,22 +45,19 @@ module ActionView # Renders the given template. A string representing the layout can be # supplied as well. - def render_template(template, layout_name = nil, locals = nil) - view, locals = @view, locals || {} - - render_with_layout(layout_name, locals) do |layout| + def render_template(view, template, layout_name, locals) + render_with_layout(view, layout_name, 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(path, locals) + def render_with_layout(view, path, locals) layout = path && find_layout(path, locals.keys, [formats.first]) content = yield(layout) if layout - view = @view view.view_flow.set(:layout, content) layout.render(view, locals) { |*name| view._layout_for(*name) } else @@ -84,9 +80,9 @@ module ActionView when String begin if layout.start_with?("/") - with_fallbacks { find_template(layout, nil, false, [], details) } + @lookup_context.with_fallbacks.find_template(layout, nil, false, [], details) else - find_template(layout, nil, false, [], details) + @lookup_context.find_template(layout, nil, false, [], details) end rescue ActionView::MissingTemplate all_details = @details.merge(formats: @lookup_context.default_formats) diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 4e5fdfbb2d..b798e80b04 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -35,47 +35,59 @@ module ActionView end module ClassMethods - def view_context_class - @view_context_class ||= begin - supports_path = supports_path? - routes = respond_to?(:_routes) && _routes - helpers = respond_to?(:_helpers) && _helpers - - Class.new(ActionView::Base) do - if routes - include routes.url_helpers(supports_path) - include routes.mounted_helpers - end - - if helpers - include helpers - end + def _routes + end + + def _helpers + end + + def build_view_context_class(klass, supports_path, routes, helpers) + Class.new(klass) do + if routes + include routes.url_helpers(supports_path) + include routes.mounted_helpers + end + + if helpers + include helpers end end end - end - attr_internal_writer :view_context_class + def view_context_class + klass = ActionView::LookupContext::DetailsKey.view_context_class(ActionView::Base) + + @view_context_class ||= build_view_context_class(klass, supports_path?, _routes, _helpers) + + if klass.changed?(@view_context_class) + @view_context_class = build_view_context_class(klass, supports_path?, _routes, _helpers) + end + + @view_context_class + end + end def view_context_class - @_view_context_class ||= self.class.view_context_class + self.class.view_context_class end # An instance of a view class. The default view class is ActionView::Base. # # The view class must have the following methods: - # View.new[lookup_context, assigns, controller] - # Create a new ActionView instance for a controller and we can also pass the arguments. - # View#render(option) - # Returns String with the rendered template + # + # * <tt>View.new(lookup_context, assigns, controller)</tt> — Create a new + # ActionView instance for a controller and we can also pass the arguments. + # + # * <tt>View#render(option)</tt> — Returns String with the rendered template. # # 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 @@ -100,7 +112,7 @@ module ActionView lookup_context.rendered_format = nil if options[:formats] lookup_context.variants = variant if variant - view_renderer.render(context, options) + context.view_renderer.render(context, options) end # Assign the rendered format to look up context. diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb index fd563f34a9..f8ea3aa770 100644 --- a/actionview/lib/action_view/routing_url_for.rb +++ b/actionview/lib/action_view/routing_url_for.rb @@ -84,25 +84,24 @@ module ActionView super(only_path: _generate_paths_by_default) when Hash options = options.symbolize_keys - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when ActionController::Parameters - unless options.key?(:only_path) - options[:only_path] = only_path?(options[:host]) - end + ensure_only_path_option(options) super(options) when :back _back_url when Array components = options.dup - if _generate_paths_by_default - polymorphic_path(components, components.extract_options!) + options = components.extract_options! + ensure_only_path_option(options) + + if options[:only_path] + polymorphic_path(components, options) else - polymorphic_url(components, components.extract_options!) + polymorphic_url(components, options) end else method = _generate_paths_by_default ? :path : :url @@ -138,8 +137,10 @@ module ActionView true end - def only_path?(host) - _generate_paths_by_default unless host + def ensure_only_path_option(options) + unless options.key?(:only_path) + options[:only_path] = _generate_paths_by_default unless options[:host] + end end end end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 0c4bb73acb..37f476e554 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -2,13 +2,23 @@ require "active_support/core_ext/object/try" require "active_support/core_ext/kernel/singleton_class" +require "active_support/deprecation" require "thread" +require "delegate" module ActionView # = Action View Template class Template extend ActiveSupport::Autoload + def self.finalize_compiled_template_methods + ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods is deprecated and has no effect" + end + + def self.finalize_compiled_template_methods=(_) + ActiveSupport::Deprecation.warn "ActionView::Template.finalize_compiled_template_methods= is deprecated and has no effect" + end + # === Encodings in ActionView::Template # # ActionView::Template is one of a few sources of potential @@ -115,15 +125,7 @@ module ActionView attr_reader :source, :identifier, :handler, :original_encoding, :updated_at - # This finalizer is needed (and exactly with a proc inside another proc) - # otherwise templates leak in development. - Finalizer = proc do |method_name, mod| # :nodoc: - proc do - mod.module_eval do - remove_possible_method method_name - end - end - end + attr_reader :variable def initialize(source, identifier, handler, details) format = details[:format] || (handler.default_format if handler.respond_to?(:default_format)) @@ -135,6 +137,13 @@ module ActionView @original_encoding = nil @locals = details[:locals] || [] @virtual_path = details[:virtual_path] + + @variable = if @virtual_path + base = @virtual_path[-1] == "/" ? "" : File.basename(@virtual_path) + base =~ /\A_?(.*?)(?:\.\w+)*\z/ + $1.to_sym + end + @updated_at = details[:updated_at] || Time.now @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f } @variants = [details[:variant]] @@ -153,10 +162,10 @@ module ActionView # This method is instrumented as "!render_template.action_view". Notice that # we use a bang in this instrumentation because you don't want to # consume this in production. This is only slow if it's being listened to. - def render(view, locals, buffer = nil, &block) + def render(view, locals, buffer = ActionView::OutputBuffer.new, &block) instrument_render_template do compile!(view) - view.send(method_name, locals, buffer, &block) + view.run(method_name, locals, buffer, &block) end rescue => e handle_render_error(view, e) @@ -186,7 +195,7 @@ module ActionView end def inspect - @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "".freeze) : identifier + @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", "") : identifier end # This method is responsible for properly setting the encoding of the @@ -200,7 +209,9 @@ module ActionView # before passing the source on to the template engine, leaving a # blank line in its stead. def encode! - return unless source.encoding == Encoding::BINARY + source = self.source + + return source unless source.encoding == Encoding::BINARY # Look for # encoding: *. If we find one, we'll encode the # String in that encoding, otherwise, we'll use the @@ -233,6 +244,19 @@ module ActionView end end + + # Exceptions are marshalled when using the parallel test runner with DRb, so we need + # to ensure that references to the template object can be marshalled as well. This means forgoing + # the marshalling of the compiler mutex and instantiating that again on unmarshalling. + def marshal_dump # :nodoc: + [ @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants ] + end + + def marshal_load(array) # :nodoc: + @source, @identifier, @handler, @compiled, @original_encoding, @locals, @virtual_path, @updated_at, @formats, @variants = *array + @compile_mutex = Mutex.new + end + private # Compile a template. This method ensures a template is compiled @@ -249,11 +273,7 @@ module ActionView # re-compilation return if @compiled - if view.is_a?(ActionView::CompiledTemplates) - mod = ActionView::CompiledTemplates - else - mod = view.singleton_class - end + mod = view.compiled_method_container instrument("!compile_template") do compile(mod) @@ -266,6 +286,15 @@ module ActionView end end + class LegacyTemplate < DelegateClass(Template) # :nodoc: + attr_reader :source + + def initialize(template, source) + super(template) + @source = source + end + end + # Among other things, this method is responsible for properly setting # the encoding of the compiled template. # @@ -279,16 +308,14 @@ module ActionView # In general, this means that templates will be UTF-8 inside of Rails, # regardless of the original source encoding. def compile(mod) - encode! - code = @handler.call(self) + source = encode! + code = @handler.call(self, source) # Make sure that the resulting String to be eval'd is in the # encoding of the code - source = <<-end_src.dup + source = +<<-end_src def #{method_name}(local_assigns, output_buffer) - _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code} - ensure - @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer + @virtual_path = #{@virtual_path.inspect};#{locals_code};#{code} end end_src @@ -303,11 +330,10 @@ module ActionView # handler is valid in the default_internal. This is for handlers # that handle encoding but screw up unless source.valid_encoding? - raise WrongEncodingError.new(@source, Encoding.default_internal) + raise WrongEncodingError.new(source, Encoding.default_internal) end mod.module_eval(source, identifier, 0) - ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) end def handle_render_error(view, e) @@ -331,19 +357,19 @@ module ActionView locals = locals.grep(/\A@?(?![A-Z0-9])(?:[[:alnum:]_]|[^\0-\177])+\z/) # Assign for the same variable is to suppress unused variable warning - locals.each_with_object("".dup) { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" } + locals.each_with_object(+"") { |key, code| code << "#{key} = local_assigns[:#{key}]; #{key} = #{key};" } end def method_name @method_name ||= begin - m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".dup - m.tr!("-".freeze, "_".freeze) + m = +"_#{identifier_method_name}__#{@identifier.hash}_#{__id__}" + m.tr!("-", "_") m end end def identifier_method_name - inspect.tr("^a-z_".freeze, "_".freeze) + inspect.tr("^a-z_", "_") end def instrument(action, &block) # :doc: @@ -351,7 +377,7 @@ module ActionView end def instrument_render_template(&block) - ActiveSupport::Notifications.instrument("!render_template.action_view".freeze, instrument_payload, &block) + ActiveSupport::Notifications.instrument("!render_template.action_view", instrument_payload, &block) end def instrument_payload diff --git a/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb index 7ec76dcc3f..ddaac7a100 100644 --- a/actionview/lib/action_view/template/handlers.rb +++ b/actionview/lib/action_view/template/handlers.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActionView #:nodoc: # = Action View Template Handlers class Template #:nodoc: @@ -14,7 +16,7 @@ module ActionView #:nodoc: base.register_template_handler :erb, ERB.new base.register_template_handler :html, Html.new base.register_template_handler :builder, Builder.new - base.register_template_handler :ruby, :source.to_proc + base.register_template_handler :ruby, lambda { |_, source| source } end @@template_handlers = {} @@ -24,11 +26,35 @@ module ActionView #:nodoc: @@template_extensions ||= @@template_handlers.keys end + class LegacyHandlerWrapper < SimpleDelegator # :nodoc: + def call(view, source) + __getobj__.call(ActionView::Template::LegacyTemplate.new(view, source)) + end + end + # Register an object that knows how to handle template files with the given # extensions. This can be used to implement new template types. # The handler must respond to +:call+, which will be passed the template # and should return the rendered template as a String. def register_template_handler(*extensions, handler) + params = if handler.is_a?(Proc) + handler.parameters + else + handler.method(:call).parameters + end + + unless params.find_all { |type, _| type == :req || type == :opt }.length >= 2 + ActiveSupport::Deprecation.warn <<~eowarn + Single arity template handlers are deprecated. Template handlers must + now accept two parameters, the view object and the source for the view object. + Change: + >> #{handler.class}#call(#{params.map(&:last).join(", ")}) + To: + >> #{handler.class}#call(#{params.map(&:last).join(", ")}, source) + eowarn + handler = LegacyHandlerWrapper.new(handler) + end + raise(ArgumentError, "Extension is required") if extensions.empty? extensions.each do |extension| @@template_handlers[extension.to_sym] = handler diff --git a/actionview/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb index 61492ce448..e5413cd2a0 100644 --- a/actionview/lib/action_view/template/handlers/builder.rb +++ b/actionview/lib/action_view/template/handlers/builder.rb @@ -5,11 +5,11 @@ module ActionView class Builder class_attribute :default_format, default: :xml - def call(template) + def call(template, source) require_engine "xml = ::Builder::XmlMarkup.new(:indent => 2);" \ "self.output_buffer = xml.target!;" + - template.source + + source + ";xml.target!;" end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index b7b749f9da..b6314a5bc3 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -14,12 +14,22 @@ module ActionView class_attribute :erb_implementation, default: Erubi # Do not escape templates of these mime types. - class_attribute :escape_whitelist, default: ["text/plain"] + class_attribute :escape_ignore_list, default: ["text/plain"] + + [self, singleton_class].each do |base| + base.alias_method :escape_whitelist, :escape_ignore_list + base.alias_method :escape_whitelist=, :escape_ignore_list= + + base.deprecate( + escape_whitelist: "use #escape_ignore_list instead", + :escape_whitelist= => "use #escape_ignore_list= instead" + ) + end ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*") - def self.call(template) - new.call(template) + def self.call(template, source) + new.call(template, source) end def supports_streaming? @@ -30,24 +40,24 @@ module ActionView true end - def call(template) + def call(template, source) # First, convert to BINARY, so in case the encoding is # wrong, we can still find an encoding tag # (<%# encoding %>) inside the String using a regular # expression - template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT) + template_source = source.dup.force_encoding(Encoding::ASCII_8BIT) erb = template_source.gsub(ENCODING_TAG, "") encoding = $2 - erb.force_encoding valid_encoding(template.source.dup, encoding) + erb.force_encoding valid_encoding(source.dup, encoding) # Always make sure we return a String in the default_internal erb.encode! self.class.erb_implementation.new( erb, - escape: (self.class.escape_whitelist.include? template.type), + escape: (self.class.escape_ignore_list.include? template.type), trim: (self.class.erb_trim_mode == "-") ).src end diff --git a/actionview/lib/action_view/template/handlers/erb/erubi.rb b/actionview/lib/action_view/template/handlers/erb/erubi.rb index db75f028ed..20510c3062 100644 --- a/actionview/lib/action_view/template/handlers/erb/erubi.rb +++ b/actionview/lib/action_view/template/handlers/erb/erubi.rb @@ -13,7 +13,7 @@ module ActionView # Dup properties so that we don't modify argument properties = Hash[properties] - properties[:preamble] = "@output_buffer = output_buffer || ActionView::OutputBuffer.new;" + properties[:preamble] = "" properties[:postamble] = "@output_buffer.to_s" properties[:bufvar] = "@output_buffer" properties[:escapefunc] = "" @@ -22,8 +22,12 @@ module ActionView end def evaluate(action_view_erb_handler_context) - pr = eval("proc { #{@src} }", binding, @filename || "(erubi)") - action_view_erb_handler_context.instance_eval(&pr) + src = @src + 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) + }.empty + view.run(:_template, {}, ActionView::OutputBuffer.new) end private diff --git a/actionview/lib/action_view/template/handlers/html.rb b/actionview/lib/action_view/template/handlers/html.rb index 27004a318c..65857d8587 100644 --- a/actionview/lib/action_view/template/handlers/html.rb +++ b/actionview/lib/action_view/template/handlers/html.rb @@ -3,7 +3,7 @@ module ActionView module Template::Handlers class Html < Raw - def call(template) + def call(template, source) "ActionView::OutputBuffer.new #{super}" end end diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 5cd23a0060..57ebb169fc 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -3,8 +3,8 @@ module ActionView module Template::Handlers class Raw - def call(template) - "#{template.source.inspect}.html_safe;" + def call(template, source) + "#{source.inspect}.html_safe;" end end end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 5a86f10973..3b4594942b 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -16,7 +16,7 @@ module ActionView alias_method :partial?, :partial def self.build(name, prefix, partial) - virtual = "".dup + virtual = +"" virtual << "#{prefix}/" unless prefix.empty? virtual << (partial ? "_#{name}" : name) new name, prefix, partial, virtual @@ -221,16 +221,13 @@ module ActionView end def query(path, details, formats, outside_app_allowed) - query = build_query(path, details) - - template_paths = find_template_paths(query) + template_paths = find_template_paths_from_details(path, details) template_paths = reject_files_external_to_app(template_paths) unless outside_app_allowed template_paths.map do |template| handler, format, variant = extract_handler_and_format_and_variant(template) - contents = File.binread(template) - Template.new(contents, File.expand_path(template), handler, + FileTemplate.new(File.expand_path(template), handler, virtual_path: path.virtual, format: format, variant: variant, @@ -243,6 +240,11 @@ module ActionView files.reject { |filename| !inside_path?(@path, filename) } end + def find_template_paths_from_details(path, details) + query = build_query(path, details) + find_template_paths(query) + end + def find_template_paths(query) Dir[query].uniq.reject do |filename| File.directory?(filename) || @@ -279,7 +281,7 @@ module ActionView end def escape_entry(entry) - entry.gsub(/[*?{}\[\]]/, '\\\\\\&'.freeze) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') end # Returns the file mtime from the filesystem. @@ -291,7 +293,7 @@ module ActionView # from the path, or the handler, we should return the array of formats given # to the resolver. def extract_handler_and_format_and_variant(path) - pieces = File.basename(path).split(".".freeze) + pieces = File.basename(path).split(".") pieces.shift extension = pieces.pop @@ -362,19 +364,56 @@ module ActionView # An Optimized resolver for Rails' most common case. class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: - def build_query(path, details) - query = escape_entry(File.join(@path, path)) + private - exts = EXTENSIONS.map do |ext, prefix| - if ext == :variants && details[ext] == :any - "{#{prefix}*,}" - else - "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + def find_template_paths_from_details(path, details) + # Instead of checking for every possible path, as our other globs would + # do, scan the directory for files with the right prefix. + query = "#{escape_entry(File.join(@path, path))}*" + + regex = build_regex(path, details) + + Dir[query].uniq.reject do |filename| + # This regex match does double duty of finding only files which match + # details (instead of just matching the prefix) and also filtering for + # case-insensitive file systems. + !regex.match?(filename) || + File.directory?(filename) + end.sort_by do |filename| + # Because we scanned the directory, instead of checking for files + # one-by-one, they will be returned in an arbitrary order. + # We can use the matches found by the regex and sort by their index in + # details. + match = filename.match(regex) + EXTENSIONS.keys.reverse.map do |ext| + if ext == :variants && details[ext] == :any + match[ext].nil? ? 0 : 1 + elsif match[ext].nil? + # No match should be last + details[ext].length + else + found = match[ext].to_sym + details[ext].index(found) + end + end end - end.join + end - query + exts - end + def build_regex(path, details) + query = escape_entry(File.join(@path, path)) + exts = EXTENSIONS.map do |ext, prefix| + match = + if ext == :variants && details[ext] == :any + ".*?" + else + details[ext].compact.uniq.map { |e| Regexp.escape(e) }.join("|") + end + prefix = Regexp.escape(prefix) + "(#{prefix}(?<#{ext}>#{match}))?" + end.join + + %r{\A#{query}#{exts}\z} + end end # The same as FileSystemResolver but does not allow templates to store diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb index e1cbae5845..e14f7aaec7 100644 --- a/actionview/lib/action_view/test_case.rb +++ b/actionview/lib/action_view/test_case.rb @@ -107,7 +107,7 @@ module ActionView # empty string ensures buffer has UTF-8 encoding as # new without arguments returns ASCII-8BIT encoded buffer like String#new @output_buffer = ActiveSupport::SafeBuffer.new "" - @rendered = "".dup + @rendered = +"" make_test_case_available_to_view! say_no_to_protect_against_forgery! diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 68186c3bf8..d6203b95c5 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -8,13 +8,15 @@ module ActionView #:nodoc: # useful for testing extensions that have no way of knowing what the file # system will look like at runtime. class FixtureResolver < PathResolver - attr_reader :hash - def initialize(hash = {}, pattern = nil) super(pattern) @hash = hash end + def data + @hash + end + def to_s @hash.keys.join(", ") end @@ -22,7 +24,7 @@ module ActionView #:nodoc: private def query(path, exts, _, _) - query = "".dup + query = +"" EXTENSIONS.each_key do |ext| query << "(" << exts[ext].map { |e| e && Regexp.escape(".#{e}") }.join("|") << "|)" end 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 |