diff options
Diffstat (limited to 'actionview')
31 files changed, 389 insertions, 59 deletions
diff --git a/actionview/.gitignore b/actionview/.gitignore index 0a04b29786..246aabbb7f 100644 --- a/actionview/.gitignore +++ b/actionview/.gitignore @@ -1,2 +1,5 @@ -/lib/assets/compiled -/tmp +/lib/assets/compiled/ +/log/ +/test/fixtures/public/absolute/ +/test/ujs/log/ +/tmp/ diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index ea7f8b205e..f44f03f40d 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,37 @@ +* Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required` + field. Example: + + select :post, + :category, + ["lifestyle", "programming", "spiritual"], + { selected: "", disabled: "", prompt: "Choose one" }, + { required: true } + + Placeholder option would be selected and disabled. The HTML produced: + + <select required="required" name="post[category]" id="post_category"> + <option disabled="disabled" selected="selected" value="">Choose one</option> + <option value="lifestyle">lifestyle</option> + <option value="programming">programming</option> + <option value="spiritual">spiritual</option></select> + + *Sergey Prikhodko* + +* Don't enforce UTF-8 by default. + + With the disabling of TLS 1.0 by most major websites, continuing to run + IE8 or lower becomes increasingly difficult so default to not enforcing + UTF-8 encoding as it's not relevant to other browsers. + + *Andrew White* + +* Change translation key of `submit_tag` from `module_name_class_name` to `module_name/class_name`. + + *Rui Onodera* + +* Rails 6 requires Ruby 2.4.1 or newer. + + *Jeremy Daer* Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/actionview/CHANGELOG.md) for previous changes. diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec index b99137fcf6..49ee1a292b 100644 --- a/actionview/actionview.gemspec +++ b/actionview/actionview.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.summary = "Rendering framework putting the V in MVC (part of Rails)." s.description = "Simple, battle-tested conventions and helpers for building web pages." - s.required_ruby_version = ">= 2.2.2" + s.required_ruby_version = ">= 2.4.1" s.license = "MIT" diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index cc0e037428..2a8f5659e3 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -1,7 +1,8 @@ +#= require ./csp #= require ./csrf #= require ./event -{ CSRFProtection, fire } = Rails +{ cspNonce, CSRFProtection, fire } = Rails AcceptHeaders = '*': '*/*' @@ -65,6 +66,7 @@ processResponse = (response, type) -> try response = JSON.parse(response) else if type.match(/\b(?:java|ecma)script\b/) script = document.createElement('script') + script.nonce = cspNonce() script.text = response document.head.appendChild(script).parentNode.removeChild(script) else if type.match(/\b(xml|html|svg)\b/) diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee new file mode 100644 index 0000000000..8d2d6ce447 --- /dev/null +++ b/actionview/app/assets/javascripts/rails-ujs/utils/csp.coffee @@ -0,0 +1,4 @@ +# Content-Security-Policy nonce for inline scripts +cspNonce = Rails.cspNonce = -> + meta = document.querySelector('meta[name=csp-nonce]') + meta and meta.content diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1cf0bd3016..dbd7a4ee11 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -45,11 +45,9 @@ module ActionView # Create a dependency tree for template named +name+. def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") + finder.formats = [finder.rendered_format] if finder.rendered_format - 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 = finder.disable_cache { finder.find_all(logical_name, [], partial, []).first } finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 46f20c4277..8cc8013718 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 @@ -46,6 +47,7 @@ module ActionView #:nodoc: include CacheHelper include CaptureHelper include ControllerHelper + include CspHelper include CsrfHelper include DateHelper include DebugHelper 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..e2e065c218 --- /dev/null +++ b/actionview/lib/action_view/helpers/csp_helper.rb @@ -0,0 +1,24 @@ +# 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 + if content_security_policy? + tag("meta", name: "csp-nonce", content: content_security_policy_nonce) + end + end + end + end +end 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 15aa9ec2dd..afd49286e6 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -610,8 +610,8 @@ module ActionView # unnecessary unless you support browsers without JavaScript. # * <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. @@ -1519,10 +1519,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) @@ -2246,7 +2246,7 @@ module ActionView @template.button_tag(value, options, &block) end - def emitted_hidden_id? + def emitted_hidden_id? # :nodoc: @emitted_hidden_id ||= nil end @@ -2266,7 +2266,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_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index e86e18dd78..54f82e058e 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. # @@ -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. @@ -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..830088bea3 100644 --- a/actionview/lib/action_view/helpers/javascript_helper.rb +++ b/actionview/lib/action_view/helpers/javascript_helper.rb @@ -63,6 +63,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,6 +79,10 @@ module ActionView content_or_options_with_block end + if html_options[:nonce] == true + html_options[:nonce] = content_security_policy_nonce + end + content_tag("script".freeze, javascript_cdata_section(content), html_options) end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index fed908fcdb..eef527d36f 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 @@ -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/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/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 1860bc4732..80cb73d683 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -122,9 +122,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 diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 73dfb267bb..12bdc642d4 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -9,6 +9,7 @@ module ActionView 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.eager_load_namespaces << ActionView @@ -35,6 +36,15 @@ 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.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb index 8a9d7982d3..e059f37d38 100644 --- a/actionview/test/actionpack/controller/render_test.rb +++ b/actionview/test/actionpack/controller/render_test.rb @@ -4,10 +4,6 @@ require "abstract_unit" require "active_model" require "controller/fake_models" -class ApplicationController < ActionController::Base - self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") -end - module Quiz # Models Question = Struct.new(:name, :id) do @@ -20,7 +16,7 @@ module Quiz end # Controller - class QuestionsController < ApplicationController + class QuestionsController < ActionController::Base def new render partial: Quiz::Question.new("Namespaced Partial") end @@ -28,7 +24,7 @@ module Quiz end module Fun - class GamesController < ApplicationController + class GamesController < ActionController::Base def hello_world; end def nested_partial_with_form_builder @@ -37,7 +33,7 @@ module Fun end end -class TestController < ApplicationController +class TestController < ActionController::Base protect_from_forgery before_action :set_variable_for_layout @@ -640,10 +636,15 @@ class RenderTest < ActionController::TestCase ActionView::Base.logger = ActiveSupport::Logger.new(nil) @request.host = "www.nextangle.com" + + @old_view_paths = ActionController::Base.view_paths + ActionController::Base.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack") end def teardown ActionView::Base.logger = nil + + ActionController::Base.view_paths = @old_view_paths end # :ported: diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb new file mode 100644 index 0000000000..12be069e69 --- /dev/null +++ b/actionview/test/activerecord/multifetch_cache_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "active_record_unit" +require "active_record/railties/collection_cache_association_loading" + +ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + +class MultifetchCacheTest < ActiveRecordTestCase + fixtures :topics, :replies + + def setup + view_paths = ActionController::Base.view_paths + + @view = Class.new(ActionView::Base) do + def view_cache_dependencies + [] + end + + def combined_fragment_cache_key(key) + [ :views, key ] + end + end.new(view_paths, {}) + end + + def test_only_preloading_for_records_that_miss_the_cache + @view.render partial: "test/partial", collection: [topics(:rails)], cached: true + + @topics = Topic.preload(:replies) + + @view.render partial: "test/partial", collection: @topics, cached: true + + assert_not @topics.detect { |topic| topic.id == topics(:rails).id }.replies.loaded? + assert @topics.detect { |topic| topic.id != topics(:rails).id }.replies.loaded? + end +end diff --git a/actionview/test/fixtures/digestor/comments/show.js.erb b/actionview/test/fixtures/digestor/comments/show.js.erb new file mode 100644 index 0000000000..38b37dfa2b --- /dev/null +++ b/actionview/test/fixtures/digestor/comments/show.js.erb @@ -0,0 +1 @@ +alert("<%=j render("comments/comment") %>") diff --git a/actionview/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore deleted file mode 100644 index 312e635ee6..0000000000 --- a/actionview/test/fixtures/public/.gitignore +++ /dev/null @@ -1 +0,0 @@ -absolute/* diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index 79e52ccc47..4b4939d705 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -141,18 +141,16 @@ class DateHelperTest < ActionView::TestCase end def test_distance_in_words_doesnt_use_the_quotient_operator - rubinius_skip "Date is written in Ruby and relies on Fixnum#/" - jruby_skip "Date is written in Ruby and relies on Fixnum#/" + rubinius_skip "Date is written in Ruby and relies on Integer#/" + jruby_skip "Date is written in Ruby and relies on Integer#/" - klass = RUBY_VERSION > "2.4" ? Integer : Fixnum - - # Make sure that we avoid {Integer,Fixnum}#/ (redefined by mathn) - klass.send :private, :/ + # Make sure that we avoid Integer#/ (redefined by mathn) + Integer.send :private, :/ from = Time.utc(2004, 6, 6, 21, 45, 0) assert_distance_of_time_in_words(from) ensure - klass.send :public, :/ + Integer.send :public, :/ end def test_time_ago_in_words_passes_include_seconds diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 1bfa39a319..ddaa7febb3 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -160,6 +160,18 @@ class TemplateDigestorTest < ActionView::TestCase assert_equal [:html], tree_template_formats("messages/show").uniq end + def test_template_dependencies_with_fallback_from_js_to_html_format + finder.rendered_format = :js + assert_equal ["comments/comment"], dependencies("comments/show") + end + + def test_template_digest_with_fallback_from_js_to_html_format + finder.rendered_format = :js + assert_digest_difference("comments/show") do + change_template("comments/_comment") + end + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored diff --git a/actionview/test/template/form_helper/form_with_test.rb b/actionview/test/template/form_helper/form_with_test.rb index 0d224d0c46..6b65d740eb 100644 --- a/actionview/test/template/form_helper/form_with_test.rb +++ b/actionview/test/template/form_helper/form_with_test.rb @@ -14,6 +14,16 @@ class FormWithTest < ActionView::TestCase teardown do ActionView::Helpers::FormHelper.form_with_generates_ids = @old_value end + + private + def with_default_enforce_utf8(value) + old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8 + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value + + yield + ensure + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value + end end class FormWithActsLikeFormTagTest < FormWithTest @@ -111,6 +121,24 @@ class FormWithActsLikeFormTagTest < FormWithTest assert_predicate actual, :html_safe? end + def test_form_with_default_enforce_utf8_false + with_default_enforce_utf8 false do + actual = form_with + expected = whole_form("http://www.example.com", skip_enforcing_utf8: true) + assert_dom_equal expected, actual + assert_predicate actual, :html_safe? + end + end + + def test_form_with_default_enforce_utf8_true + with_default_enforce_utf8 true do + actual = form_with + expected = whole_form("http://www.example.com", skip_enforcing_utf8: false) + assert_dom_equal expected, actual + assert_predicate actual, :html_safe? + end + end + def test_form_with_with_block_in_erb output_buffer = render_erb("<%= form_with(url: 'http://www.example.com') do %>Hello world!<% end %>") @@ -190,6 +218,9 @@ class FormWithActsLikeFormForTest < FormWithTest submit: "Save changes", another_post: { update: "Update your %{model}" + }, + "blog/post": { + update: "Update your %{model}" } } } @@ -816,6 +847,34 @@ class FormWithActsLikeFormForTest < FormWithTest assert_dom_equal expected, output_buffer end + def test_form_with_default_enforce_utf8_true + with_default_enforce_utf8 true do + form_with(scope: :post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", skip_enforcing_utf8: false) do + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_form_with_default_enforce_utf8_false + with_default_enforce_utf8 false do + form_with(scope: :post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", skip_enforcing_utf8: true) do + "<input name='post[title]' type='text' value='Hello World' id='post_title' />" + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_with_without_object form_with(scope: :post, id: "create-post") do |f| concat f.text_field(:title) @@ -962,7 +1021,7 @@ class FormWithActsLikeFormForTest < FormWithTest end end - def test_submit_with_object_and_nested_lookup + def test_submit_with_object_which_is_overwritten_by_scope_option with_locale :submit do form_with(model: @post, scope: :another_post) do |f| concat f.submit @@ -976,6 +1035,21 @@ class FormWithActsLikeFormForTest < FormWithTest end end + def test_submit_with_object_which_is_namespaced + blog_post = Blog::Post.new("And his name will be forty and four.", 44) + with_locale :submit do + form_with(model: blog_post) do |f| + concat f.submit + end + + expected = whole_form("/posts/44", method: "patch") do + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" + end + + assert_dom_equal expected, output_buffer + end + end + def test_fields_with_attributes_not_on_model form_with(model: @post) do |f| concat f.fields(:comment) { |c| @@ -2273,4 +2347,13 @@ class FormWithActsLikeFormForTest < FormWithTest ensure I18n.locale = old_locale end + + def with_default_enforce_utf8(value) + old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8 + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value + + yield + ensure + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value + end end diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index b8fad090c5..5244204e42 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -68,6 +68,9 @@ class FormHelperTest < ActionView::TestCase submit: "Save changes", another_post: { update: "Update your %{model}" + }, + "blog/post": { + update: "Update your %{model}" } } } @@ -1992,6 +1995,34 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, output_buffer end + def test_form_for_default_enforce_utf8_false + with_default_enforce_utf8 false do + form_for(:post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", nil, nil, enforce_utf8: false) do + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + end + + def test_form_for_default_enforce_utf8_true + with_default_enforce_utf8 true do + form_for(:post) do |f| + concat f.text_field(:title) + end + + expected = whole_form("/", nil, nil, enforce_utf8: true) do + "<input name='post[title]' type='text' id='post_title' value='Hello World' />" + end + + assert_dom_equal expected, output_buffer + end + end + def test_form_for_with_remote_in_html form_for(@post, url: "/", html: { remote: true, id: "create-post", method: :patch }) do |f| concat f.text_field(:title) @@ -2271,7 +2302,7 @@ class FormHelperTest < ActionView::TestCase end end - def test_submit_with_object_and_nested_lookup + def test_submit_with_object_which_is_overwritten_by_as_option with_locale :submit do form_for(@post, as: :another_post) do |f| concat f.submit @@ -2285,6 +2316,21 @@ class FormHelperTest < ActionView::TestCase end end + def test_submit_with_object_which_is_namespaced + blog_post = Blog::Post.new("And his name will be forty and four.", 44) + with_locale :submit do + form_for(blog_post) do |f| + concat f.submit + end + + expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do + "<input name='commit' data-disable-with='Update your Post' type='submit' value='Update your Post' />" + end + + assert_dom_equal expected, output_buffer + end + end + def test_nested_fields_for @comment.body = "Hello World" form_for(@post) do |f| @@ -3551,4 +3597,13 @@ class FormHelperTest < ActionView::TestCase ensure I18n.locale = old_locale end + + def with_default_enforce_utf8(value) + old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8 + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value + + yield + ensure + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value + end end diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index f82eada869..8f796bdb83 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -511,6 +511,16 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_required_select_with_default_and_selected_placeholder + assert_dom_equal( + ['<select required="required" name="post[category]" id="post_category"><option disabled="disabled" selected="selected" value="">Choose one</option>', + '<option value="lifestyle">lifestyle</option>', + '<option value="programming">programming</option>', + '<option value="spiritual">spiritual</option></select>'].join("\n"), + select(:post, :category, ["lifestyle", "programming", "spiritual"], { selected: "", disabled: "", prompt: "Choose one" }, { required: true }) + ) + end + def test_select_with_grouped_collection_as_nested_array @post = Post.new diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 0d9bf77f98..a3500a7eb3 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -152,6 +152,24 @@ class FormTagHelperTest < ActionView::TestCase assert_predicate actual, :html_safe? end + def test_form_tag_default_enforce_utf8_false + with_default_enforce_utf8 false do + actual = form_tag({}) + expected = whole_form("http://www.example.com", enforce_utf8: false) + assert_dom_equal expected, actual + assert_predicate actual, :html_safe? + end + end + + def test_form_tag_default_enforce_utf8_true + with_default_enforce_utf8 true do + actual = form_tag({}) + expected = whole_form("http://www.example.com", enforce_utf8: true) + assert_dom_equal expected, actual + assert_predicate actual, :html_safe? + end + end + def test_form_tag_with_block_in_erb output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>") @@ -782,4 +800,13 @@ class FormTagHelperTest < ActionView::TestCase def root_elem(rendered_content) Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset end + + def with_default_enforce_utf8(value) + old_value = ActionView::Helpers::FormTagHelper.default_enforce_utf8 + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = value + + yield + ensure + ActionView::Helpers::FormTagHelper.default_enforce_utf8 = old_value + end end diff --git a/actionview/test/tmp/.keep b/actionview/test/tmp/.keep deleted file mode 100644 index e69de29bb2..0000000000 --- a/actionview/test/tmp/.keep +++ /dev/null diff --git a/actionview/test/ujs/.gitignore b/actionview/test/ujs/.gitignore deleted file mode 100644 index 31dbbff57c..0000000000 --- a/actionview/test/ujs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/log diff --git a/actionview/test/ujs/public/test/call-ajax.js b/actionview/test/ujs/public/test/call-ajax.js index 49e64cad5c..4d0bfb0806 100644 --- a/actionview/test/ujs/public/test/call-ajax.js +++ b/actionview/test/ujs/public/test/call-ajax.js @@ -8,7 +8,6 @@ module('call-ajax', { }) asyncTest('call ajax without "ajax:beforeSend"', 1, function() { - var link = $('#qunit-fixture a') link.bindNative('click', function() { Rails.ajax({ @@ -21,7 +20,7 @@ asyncTest('call ajax without "ajax:beforeSend"', 1, function() { }) link.triggerNative('click') - setTimeout(function() { start() }, 13) + setTimeout(function() { start() }, 50) }) })() diff --git a/actionview/test/ujs/server.rb b/actionview/test/ujs/server.rb index 7d1bab4b2a..48e9bcb65f 100644 --- a/actionview/test/ujs/server.rb +++ b/actionview/test/ujs/server.rb @@ -23,18 +23,30 @@ module UJS config.public_file_server.enabled = true config.logger = Logger.new(STDOUT) config.log_level = :error + + config.content_security_policy do |policy| + policy.default_src :self, :https + policy.font_src :self, :https, :data + policy.img_src :self, :https, :data + policy.object_src :none + policy.script_src :self, :https + policy.style_src :self, :https + end + + config.content_security_policy_nonce_generator = ->(req) { SecureRandom.base64(16) } end end module TestsHelper def test_to(*names) - names = ["/vendor/qunit.js", "settings"] + names - names.map { |name| script_tag name }.join("\n").html_safe - end + names = names.map { |name| "/test/#{name}.js" } + names = %w[/vendor/qunit.js /test/settings.js] + names - def script_tag(src) - src = "/test/#{src}.js" unless src.index("/") - %(<script src="#{src}" type="text/javascript"></script>).html_safe + capture do + names.each do |name| + concat(javascript_include_tag(name)) + end + end end end @@ -56,7 +68,7 @@ class TestsController < ActionController::Base elsif params[:iframe] payload = JSON.generate(data).gsub("<", "<").gsub(">", ">") html = <<-HTML - <script> + <script nonce="#{request.content_security_policy_nonce}"> if (window.top && window.top !== window) window.top.jQuery.event.trigger('iframe:loaded', #{payload}) </script> diff --git a/actionview/test/ujs/views/layouts/application.html.erb b/actionview/test/ujs/views/layouts/application.html.erb index c787e77b84..8f6f6fc17f 100644 --- a/actionview/test/ujs/views/layouts/application.html.erb +++ b/actionview/test/ujs/views/layouts/application.html.erb @@ -2,9 +2,10 @@ <html id="html"> <head> <title><%= @title %></title> + <%= csp_meta_tag %> <link href="/vendor/qunit.css" media="screen" rel="stylesheet" type="text/css" media="screen, projection" /> <script src="/vendor/jquery-2.2.0.js" type="text/javascript"></script> - <script> + <%= javascript_tag nonce: true do %> // This is for test in override.js. // Must go before rails-ujs. document.addEventListener('rails:attachBindings', function() { @@ -15,8 +16,8 @@ e.preventDefault(); }); }); - </script> - <%= script_tag "/rails-ujs.js" %> + <% end %> + <%= javascript_include_tag "/rails-ujs.js" %> </head> <body id="body"> |