diff options
Diffstat (limited to 'actionview/lib')
13 files changed, 170 insertions, 35 deletions
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/digestor.rb b/actionview/lib/action_view/digestor.rb index 39cdecb9e4..6d2e471a44 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -18,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) @@ -30,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 diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 14bd8ffa84..cbcce4a4dc 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,7 +55,7 @@ module ActionView # 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 + # * <tt>:nonce</tt> - When set to true, adds an automatic nonce value if # you have Content Security Policy enabled. # # ==== Examples diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 15d187a9ec..b1a14250c3 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -208,27 +208,35 @@ module ActionView # # 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/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 07f3d98322..6e769aa560 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -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 diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 7884a8d997..2b9d55a019 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -794,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 @@ -802,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) 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/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb index cb0c99c4cf..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) %> # diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 77a1c1fed9..a338d076e4 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -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 =~ /^1(\.0+)?$/ singular else plural || singular.pluralize(locale) 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..5aa6f77902 100644 --- a/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb +++ b/actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb @@ -14,15 +14,35 @@ module ActionView def cache_collection_render(instrumentation_payload) return yield unless @options[:cached] + # 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 + + # 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 @@ -40,10 +60,29 @@ module ActionView end def expanded_cache_key(key) - key = @view.combined_fragment_cache_key(@view.cache_fragment_name(key, virtual_path: @template.virtual_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 + def digest_path + @digest_path ||= @view.digest_path_from_virtual(@template.virtual_path) + 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/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index b7b749f9da..270be0a380 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -14,7 +14,17 @@ 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.send(:alias_method, :escape_whitelist, :escape_ignore_list) + base.send(: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]*") @@ -47,7 +57,7 @@ module ActionView 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/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 5a86f10973..db3cff71df 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -221,9 +221,7 @@ 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| @@ -243,6 +241,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) || @@ -362,19 +365,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. + !filename.match(regex) || + 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 |