diff options
39 files changed, 602 insertions, 61 deletions
diff --git a/actionmailbox/CHANGELOG.md b/actionmailbox/CHANGELOG.md index bca3d1d9ed..0d4799a7b8 100644 --- a/actionmailbox/CHANGELOG.md +++ b/actionmailbox/CHANGELOG.md @@ -1,3 +1,7 @@ +* Fix Bcc header not being included with emails from `create_inbound_email_from` test helpers. + + *jduff* + * Add `ApplicationMailbox.mailbox_for` to expose mailbox routing. *James Dabbs* diff --git a/actionmailbox/lib/action_mailbox/test_helper.rb b/actionmailbox/lib/action_mailbox/test_helper.rb index d248fa8734..dc6a0229f7 100644 --- a/actionmailbox/lib/action_mailbox/test_helper.rb +++ b/actionmailbox/lib/action_mailbox/test_helper.rb @@ -14,7 +14,11 @@ module ActionMailbox # # create_inbound_email_from_mail(from: "david@loudthinking.com", subject: "Hello!") def create_inbound_email_from_mail(status: :processing, **mail_options) - create_inbound_email_from_source Mail.new(mail_options).to_s, status: status + mail = Mail.new(mail_options) + # Bcc header is not encoded by default + mail[:bcc].include_in_headers = true if mail[:bcc] + + create_inbound_email_from_source mail.to_s, status: status end # Create an +InboundEmail+ using the raw rfc822 +source+ as text. diff --git a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb index 06f5a9d6db..33282d36f7 100644 --- a/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb +++ b/actionmailbox/test/controllers/rails/action_mailbox/inbound_emails_controller_test.rb @@ -30,6 +30,27 @@ class Rails::Conductor::ActionMailbox::InboundEmailsControllerTest < ActionDispa end end + test "create inbound email with bcc" do + with_rails_env("development") do + assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do + post rails_conductor_inbound_emails_path, params: { + mail: { + from: "Jason Fried <jason@37signals.com>", + bcc: "Replies <replies@example.com>", + subject: "Hey there", + body: "How's it going?" + } + } + end + + mail = ActionMailbox::InboundEmail.last.mail + assert_equal %w[ jason@37signals.com ], mail.from + assert_equal %w[ replies@example.com ], mail.bcc + assert_equal "Hey there", mail.subject + assert_equal "How's it going?", mail.body.decoded + end + end + test "create inbound email with attachments" do with_rails_env("development") do assert_difference -> { ActionMailbox::InboundEmail.count }, +1 do diff --git a/actionmailbox/test/unit/router_test.rb b/actionmailbox/test/unit/router_test.rb index 4dd3730604..7eb8e04a73 100644 --- a/actionmailbox/test/unit/router_test.rb +++ b/actionmailbox/test/unit/router_test.rb @@ -51,6 +51,15 @@ module ActionMailbox assert_equal inbound_email.mail, $processed_mail end + test "single string routing on bcc" do + @router.add_routes("first@example.com" => :first) + + inbound_email = create_inbound_email_from_mail(to: "someone@example.com", bcc: "first@example.com", subject: "This is a reply") + @router.route inbound_email + assert_equal "FirstMailbox", $processed_by + assert_equal inbound_email.mail, $processed_mail + end + test "single string routing case-insensitively" do @router.add_routes("first@example.com" => :first) diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index ba914c4813..eb0885953b 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -737,7 +737,7 @@ module ActionMailer end class LateAttachmentsProxy < SimpleDelegator - def inline; _raise_error end + def inline; self end def []=(_name, _content); _raise_error end private diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb index 4ea5634cd0..a6223964e5 100644 --- a/actionmailer/test/base_test.rb +++ b/actionmailer/test/base_test.rb @@ -267,6 +267,17 @@ class BaseTest < ActiveSupport::TestCase assert_match(/Can't add attachments after `mail` was called./, e.message) end + test "accessing inline attachments after mail was called works" do + class LateInlineAttachmentMailer < ActionMailer::Base + def welcome + mail body: "yay", from: "welcome@example.com", to: "to@example.com" + attachments.inline["invoice.pdf"] + end + end + + assert_nothing_raised { LateInlineAttachmentMailer.welcome.message } + end + test "adding inline attachments while rendering mail works" do class LateInlineAttachmentMailer < ActionMailer::Base def on_render diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index ca7ae6621b..8bb2c73e52 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,18 @@ +* Add `Vary: Accept` header when using `Accept` header for response + + For some requests like `/users/1`, Rails uses requests' `Accept` + header to determine what to return. And if we don't add `Vary` + in the response header, browsers might accidentally cache different + types of content, which would cause issues: e.g. javascript got displayed + instead of html content. This PR fixes these issues by adding `Vary: Accept` + in these types of requests. For more detailed problem description, please read: + + https://github.com/rails/rails/pull/36213 + + Fixes #25842 + + *Stan Lo* + * Fix IntegrationTest `follow_redirect!` to follow redirection using the same HTTP verb when following a 307 redirection. diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index 8ba2b25552..280af50a44 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -28,6 +28,7 @@ module AbstractController else _set_rendered_content_type rendered_format end + _set_vary_header self.response_body = rendered_body end @@ -109,6 +110,9 @@ module AbstractController def _set_html_content_type # :nodoc: end + def _set_vary_header # :nodoc: + end + def _set_rendered_content_type(format) # :nodoc: end diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb index efa5de313c..fd22c4fa64 100644 --- a/actionpack/lib/action_controller/metal/rendering.rb +++ b/actionpack/lib/action_controller/metal/rendering.rb @@ -77,6 +77,10 @@ module ActionController end end + def _set_vary_header + self.headers["Vary"] = "Accept" if request.should_apply_vary_header? + end + # Normalize arguments by catching blocks and setting them on :update. def _normalize_args(action = nil, options = {}, &blk) options = super diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index a2cac49082..ac0ff133eb 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -62,13 +62,7 @@ module ActionDispatch def formats fetch_header("action_dispatch.request.formats") do |k| - params_readable = begin - parameters[:format] - rescue *RESCUABLE_MIME_FORMAT_ERRORS - false - end - - v = if params_readable + v = if params_readable? Array(Mime[parameters[:format]]) elsif use_accept_header && valid_accept_header accepts @@ -153,9 +147,19 @@ module ActionDispatch order.include?(Mime::ALL) ? format : nil end + def should_apply_vary_header? + !params_readable? && use_accept_header && valid_accept_header + end + private BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/ + def params_readable? # :doc: + parameters[:format] + rescue *RESCUABLE_MIME_FORMAT_ERRORS + false + end + def valid_accept_header # :doc: (xhr? && (accept.present? || content_mime_type)) || (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS) diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb index 4aee7a6f83..9184676801 100644 --- a/actionpack/lib/action_dispatch/journey/route.rb +++ b/actionpack/lib/action_dispatch/journey/route.rb @@ -148,7 +148,7 @@ module ActionDispatch end def glob? - !path.spec.grep(Nodes::Star).empty? + path.spec.any?(Nodes::Star) end def dispatcher? diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index caacd66bd6..cb7c2467ac 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -543,6 +543,32 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest end end + def test_setting_vary_header_when_request_is_xhr_with_accept_header + with_test_route_set do + get "/get", headers: { "Accept" => "application/json" }, xhr: true + assert_equal "Accept", response.headers["Vary"] + end + end + + def test_not_setting_vary_header_when_format_is_provided + with_test_route_set do + get "/get", params: { format: "json" } + assert_nil response.headers["Vary"] + end + end + + def test_not_setting_vary_header_when_ignore_accept_header_is_set + original_ignore_accept_header = ActionDispatch::Request.ignore_accept_header + ActionDispatch::Request.ignore_accept_header = true + + with_test_route_set do + get "/get", headers: { "Accept" => "application/json" }, xhr: true + assert_nil response.headers["Vary"] + end + ensure + ActionDispatch::Request.ignore_accept_header = original_ignore_accept_header + end + private def with_default_headers(headers) original = ActionDispatch::Response.default_headers diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index 63c147cb1b..f20043b9ac 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -304,7 +304,7 @@ module TestGenerationPrefix assert_equal "/omg/blog/posts/1", path end - test "[OBJECT] generating engine's route with named helpers" do + test "[OBJECT] generating engine's route with named route helpers" do path = engine_object.posts_path assert_equal "/awesome/blog/posts", path diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 0ec8dd25e0..c4cb27429d 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -865,12 +865,28 @@ class RequestFormat < BaseRequestTest assert_not_predicate request.format, :json? end - test "format does not throw exceptions when malformed parameters" do + test "format does not throw exceptions when malformed GET parameters" do request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2") assert request.formats assert_predicate request.format, :html? end + test "format does not throw exceptions when invalid POST parameters" do + body = "{record:{content:127.0.0.1}}" + request = stub_request( + "REQUEST_METHOD" => "POST", + "CONTENT_LENGTH" => body.length, + "CONTENT_TYPE" => "application/json", + "rack.input" => StringIO.new(body), + "action_dispatch.logger" => Logger.new(output = StringIO.new) + ) + assert request.formats + assert request.format.html? + + output.rewind && (err = output.read) + assert_match /Error occurred while parsing request parameters/, err + end + test "formats with xhr request" do request = stub_request "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", "QUERY_STRING" => "" diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 504b1d3e98..ca3ce1476a 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,7 @@ +* Added `phone_to` helper method to create a link from mobile numbers + + *Pietro Moro* + * annotated_source_code returns an empty array so TemplateErrors without a template in the backtrace are surfaced properly by DebugExceptions. diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee index 736cab08db..9e11bfa7ed 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/form.coffee @@ -11,6 +11,7 @@ Rails.serializeElement = (element, additionalParam) -> inputs.forEach (input) -> return if !input.name || input.disabled + return if input.closest('fieldset[disabled]') if matches(input, 'select') toArray(input.options).forEach (option) -> params.push(name: input.name, value: option.value) if option.selected diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 7517410ea5..cdf436ccae 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -13,7 +13,7 @@ module ActionView # * <tt>format</tt> - Template format # * <tt>finder</tt> - An instance of <tt>ActionView::LookupContext</tt> # * <tt>dependencies</tt> - An array of dependent views - def digest(name:, format:, finder:, dependencies: nil) + def digest(name:, format: nil, finder:, dependencies: nil) if dependencies.nil? || dependencies.empty? cache_key = "#{name}.#{format}" else diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index a7747456a4..ad19426fff 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -566,9 +566,10 @@ module ActionView # an ActiveSupport::TimeZone. # # By default, +model+ is the ActiveSupport::TimeZone constant (which can - # be obtained in Active Record as a value object). The only requirement - # is that the +model+ parameter be an object that responds to +all+, and - # returns an array of objects that represent time zones. + # be obtained in Active Record as a value object). The +model+ parameter + # must respond to +all+ and return an array of objects that represent time + # zones; each object must respond to +name+. If a Regexp is given it will + # attempt to match the zones using the <code>=~<code> operator. # # NOTE: Only the option tags are returned, you have to wrap this call in # a regular HTML select tag. diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 85fd549177..9b4116834e 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -619,6 +619,53 @@ module ActionView content_tag("a", name || phone_number, html_options, &block) end + # Creates a TEL anchor link tag to the specified +phone_number+, which is + # also used as the name of the link unless +name+ is specified. Additional + # HTML attributes for the link can be passed in +html_options+. + # + # When clicked, the default app to make calls is opened, and it + # is prepopulated with the passed phone number and optional + # +country_code+ value. + # + # +phone_to+ has a optional +country_code+ option which automatically adds the country + # code as well as the + sign in the phone numer that gets prepopulated, + # for example if +country_code: "01"+ +\+01+ will be prepended to the + # phone numer, by passing special keys to +html_options+. + # + # ==== Options + # * <tt>:country_code</tt> - Prepends the country code to the number + # + # ==== Examples + # phone_to "1234567890" + # # => <a href="tel:1234567890">1234567890</a> + # + # phone_to "1234567890", "Phone me" + # # => <a href="tel:134567890">Phone me</a> + # + # phone_to "1234567890", "Phone me", country_code: "01" + # # => <a href="tel:+015155555785">Phone me</a> + # + # You can use a block as well if your link target is hard to fit into the name parameter. \ERB example: + # + # <%= phone_to "1234567890" do %> + # <strong>Phone me:</strong> + # <% end %> + # # => <a href="tel:1234567890"> + # <strong>Phone me:</strong> + # </a> + def phone_to(phone_number, name = nil, html_options = {}, &block) + html_options, name = name, nil if block_given? + html_options = (html_options || {}).stringify_keys + + country_code = html_options.delete("country_code").presence + country_code = country_code.nil? ? "" : "+#{ERB::Util.url_encode(country_code)}" + + encoded_phone_number = ERB::Util.url_encode(phone_number) + html_options["href"] = "tel:#{country_code}#{encoded_phone_number}" + + content_tag("a", name || phone_number, html_options, &block) + end + private def convert_options_to_data_attributes(options, html_options) if html_options diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index bce6e7f370..08413a65c8 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -731,45 +731,109 @@ class UrlHelperTest < ActiveSupport::TestCase ) end - def test_sms_with_img + def test_sms_to_with_img assert_dom_equal %{<a href="sms:15155555785;"><img src="/feedback.png" /></a>}, sms_to("15155555785", raw('<img src="/feedback.png" />')) end - def test_sms_with_html_safe_string + def test_sms_to_with_html_safe_string assert_dom_equal( %{<a href="sms:1%2B5155555785;">1+5155555785</a>}, sms_to(raw("1+5155555785")) ) end - def test_sms_with_nil + def test_sms_to_with_nil assert_dom_equal( %{<a href="sms:;"></a>}, sms_to(nil) ) end - def test_sms_returns_html_safe_string + def test_sms_to_returns_html_safe_string assert_predicate sms_to("15155555785"), :html_safe? end - def test_sms_with_block + def test_sms_to_with_block assert_dom_equal %{<a href="sms:15155555785;"><span>Text me</span></a>}, sms_to("15155555785") { content_tag(:span, "Text me") } end - def test_sms_with_block_and_options + def test_sms_to_with_block_and_options assert_dom_equal %{<a class="special" href="sms:15155555785;?&body=Hello%20from%20Jim"><span>Text me</span></a>}, sms_to("15155555785", body: "Hello from Jim", class: "special") { content_tag(:span, "Text me") } end - def test_sms_does_not_modify_html_options_hash + def test_sms_to_does_not_modify_html_options_hash options = { class: "special" } sms_to "15155555785", "ME!", options assert_equal({ class: "special" }, options) end + def test_phone_to + assert_dom_equal %{<a href="tel:1234567890">1234567890</a>}, + phone_to("1234567890") + assert_dom_equal %{<a href="tel:1234567890">Bob</a>}, + phone_to("1234567890", "Bob") + assert_dom_equal( + %{<a class="phoner" href="tel:1234567890">Bob</a>}, + phone_to("1234567890", "Bob", "class" => "phoner") + ) + assert_equal phone_to("1234567890", "Bob", "class" => "admin"), + phone_to("1234567890", "Bob", class: "admin") + end + + def test_phone_to_with_options + assert_dom_equal( + %{<a class="example-class" href="tel:+011234567890">Phone</a>}, + phone_to("1234567890", "Phone", class: "example-class", country_code: "01") + ) + + assert_dom_equal( + %{<a href="tel:+011234567890">Phone</a>}, + phone_to("1234567890", "Phone", country_code: "01") + ) + end + + def test_phone_with_img + assert_dom_equal %{<a href="tel:1234567890"><img src="/feedback.png" /></a>}, + phone_to("1234567890", raw('<img src="/feedback.png" />')) + end + + def test_phone_with_html_safe_string + assert_dom_equal( + %{<a href="tel:1%2B234567890">1+234567890</a>}, + phone_to(raw("1+234567890")) + ) + end + + def test_phone_with_nil + assert_dom_equal( + %{<a href="tel:"></a>}, + phone_to(nil) + ) + end + + def test_phone_returns_html_safe_string + assert_predicate phone_to("1234567890"), :html_safe? + end + + def test_phone_with_block + assert_dom_equal %{<a href="tel:1234567890"><span>Phone</span></a>}, + phone_to("1234567890") { content_tag(:span, "Phone") } + end + + def test_phone_with_block_and_options + assert_dom_equal %{<a class="special" href="tel:+011234567890"><span>Phone</span></a>}, + phone_to("1234567890", country_code: "01", class: "special") { content_tag(:span, "Phone") } + end + + def test_phone_does_not_modify_html_options_hash + options = { class: "special" } + phone_to "1234567890", "ME!", options + assert_equal({ class: "special" }, options) + end + def protect_against_forgery? request_forgery end diff --git a/actionview/test/ujs/public/test/data-remote.js b/actionview/test/ujs/public/test/data-remote.js index 16ea114f3b..dbbb383995 100644 --- a/actionview/test/ujs/public/test/data-remote.js +++ b/actionview/test/ujs/public/test/data-remote.js @@ -490,4 +490,22 @@ asyncTest('changing a select option without "data-url" attribute still fires aja setTimeout(function() { start() }, 20) }) +asyncTest('inputs inside disabled fieldset are not submited on remote forms', 3, function() { + $('form') + .append('<fieldset>\ + <input name="description" value="A wise man" />\ + </fieldset>') + .append('<fieldset disabled="disabled">\ + <input name="age" />\ + </fieldset>') + .bindNative('ajax:success', function(e, data, status, xhr) { + equal(data.params.user_name, 'john') + equal(data.params.description, 'A wise man') + equal(data.params.age, undefined) + + start() + }) + .triggerNative('submit') +}) + })() diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index 6ad4c75fb5..2072f93194 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -62,7 +62,7 @@ module ActiveRecord::Associations::Builder # :nodoc: def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join("_").gsub("::", "_").to_sym + association_name.to_s].sort.join("_").gsub("::", "_").to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index db76cc8a7e..fcf13a910c 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -91,6 +91,19 @@ module ActiveRecord end alias :blank? :empty? + def each + throw_getter_deprecation(:each) + configurations.each { |config| + yield [config.env_name, config.config] + } + end + + def first + throw_getter_deprecation(:first) + config = configurations.first + [config.env_name, config.config] + end + private def env_with_configs(env = nil) if env @@ -183,9 +196,6 @@ module ActiveRecord def method_missing(method, *args, &blk) case method - when :each, :first - throw_getter_deprecation(method) - configurations.send(method, *args, &blk) when :fetch throw_getter_deprecation(method) configs_for(env_name: args.first) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 25cfa0a723..de9742b250 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -700,10 +700,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal ["id"], developers(:david).projects.select(:id).first.attributes.keys end + def test_join_middle_table_alias + assert_equal( + 2, + Project.includes(:developers_projects).where.not("developers_projects.joined_on": nil).to_a.size + ) + end + def test_join_table_alias assert_equal( 3, - Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).to_a.size + Developer.includes(projects: :developers).where.not("developers_projects_projects_join.joined_on": nil).to_a.size ) end @@ -716,7 +723,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, - Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).group(group.join(",")).to_a.size + Developer.includes(projects: :developers).where.not("developers_projects_projects_join.joined_on": nil).group(group.join(",")).to_a.size ) end diff --git a/activerecord/test/cases/database_configurations_test.rb b/activerecord/test/cases/database_configurations_test.rb index ed8151f01a..725d1b5d1b 100644 --- a/activerecord/test/cases/database_configurations_test.rb +++ b/activerecord/test/cases/database_configurations_test.rb @@ -80,17 +80,20 @@ class LegacyDatabaseConfigurationsTest < ActiveRecord::TestCase def test_each_is_deprecated assert_deprecated do - ActiveRecord::Base.configurations.each do |db_config| - assert_equal "primary", db_config.spec_name + all_configs = ActiveRecord::Base.configurations.values + ActiveRecord::Base.configurations.each do |env_name, config| + assert_includes ["arunit", "arunit2", "arunit_without_prepared_statements"], env_name + assert_includes all_configs, config end end end def test_first_is_deprecated + first_config = ActiveRecord::Base.configurations.values.first assert_deprecated do - db_config = ActiveRecord::Base.configurations.first - assert_equal "arunit", db_config.env_name - assert_equal "primary", db_config.spec_name + env_name, config = ActiveRecord::Base.configurations.first + assert_equal "arunit", env_name + assert_equal first_config, config end end diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb index 4894193665..4f70e7f5fc 100644 --- a/activesupport/lib/active_support/core_ext/string/access.rb +++ b/activesupport/lib/active_support/core_ext/string/access.rb @@ -61,8 +61,8 @@ class String # str.from(0).to(-1) # => "hello" # str.from(1).to(-2) # => "ell" def to(position) - position = [position + length, -1].max if position < 0 - self[0, position + 1] + position += size if position < 0 + self[0, position + 1].to_s end # Returns the first character. If a limit is supplied, returns a substring diff --git a/activesupport/lib/active_support/inflector/transliterate.rb b/activesupport/lib/active_support/inflector/transliterate.rb index ec6e9ccb59..0751f8a3ad 100644 --- a/activesupport/lib/active_support/inflector/transliterate.rb +++ b/activesupport/lib/active_support/inflector/transliterate.rb @@ -56,14 +56,39 @@ module ActiveSupport # # transliterate('Jürgen', locale: :de) # # => "Juergen" + # + # Transliteration is restricted to UTF-8, US-ASCII and GB18030 strings + # Other encodings will raise an ArgumentError. def transliterate(string, replacement = "?", locale: nil) raise ArgumentError, "Can only transliterate strings. Received #{string.class.name}" unless string.is_a?(String) - I18n.transliterate( + allowed_encodings = [Encoding::UTF_8, Encoding::US_ASCII, Encoding::GB18030] + raise ArgumentError, "Can not transliterate strings with #{string.encoding} encoding" unless allowed_encodings.include?(string.encoding) + + input_encoding = string.encoding + + # US-ASCII is a subset of UTF-8 so we'll force encoding as UTF-8 if + # US-ASCII is given. This way we can let tidy_bytes handle the string + # in the same way as we do for UTF-8 + string.force_encoding(Encoding::UTF_8) if string.encoding == Encoding::US_ASCII + + # GB18030 is Unicode compatible but is not a direct mapping so needs to be + # transcoded. Using invalid/undef :replace will result in loss of data in + # the event of invalid characters, but since tidy_bytes will replace + # invalid/undef with a "?" we're safe to do the same beforehand + string.encode!(Encoding::UTF_8, invalid: :replace, undef: :replace) if string.encoding == Encoding::GB18030 + + transliterated = I18n.transliterate( ActiveSupport::Multibyte::Unicode.tidy_bytes(string).unicode_normalize(:nfc), replacement: replacement, locale: locale ) + + # Restore the string encoding of the input if it was not UTF-8. + # Apply invalid/undef :replace as tidy_bytes does + transliterated.encode!(input_encoding, invalid: :replace, undef: :replace) if input_encoding != transliterated.encoding + + transliterated end # Replaces special characters in a string so that it may be used as part of diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index c5a000b67a..b1ab9533e7 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -455,6 +455,8 @@ class StringAccessTest < ActiveSupport::TestCase test "#to with negative Integer, position is counted from the end" do assert_equal "hell", "hello".to(-2) + assert_equal "h", "hello".to(-5) + assert_equal "", "hello".to(-7) end test "#from and #to can be combined" do diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb index 9e29a93ea0..2e02b5e938 100644 --- a/activesupport/test/transliterate_test.rb +++ b/activesupport/test/transliterate_test.rb @@ -57,4 +57,53 @@ class TransliterateTest < ActiveSupport::TestCase end assert_equal "Can only transliterate strings. Received Object", exception.message end + + def test_transliterate_handles_strings_with_valid_utf8_encodings + string = String.new("A", encoding: Encoding::UTF_8) + assert_equal "A", ActiveSupport::Inflector.transliterate(string) + end + + def test_transliterate_handles_strings_with_valid_us_ascii_encodings + string = String.new("A", encoding: Encoding::US_ASCII) + transcoded = ActiveSupport::Inflector.transliterate(string) + assert_equal "A", transcoded + assert_equal Encoding::US_ASCII, transcoded.encoding + end + + def test_transliterate_handles_strings_with_valid_gb18030_encodings + string = String.new("A", encoding: Encoding::GB18030) + transcoded = ActiveSupport::Inflector.transliterate(string) + assert_equal "A", transcoded + assert_equal Encoding::GB18030, transcoded.encoding + end + + def test_transliterate_handles_strings_with_incompatible_encodings + incompatible_encodings = Encoding.list - [ + Encoding::UTF_8, + Encoding::US_ASCII, + Encoding::GB18030 + ] + incompatible_encodings.each do |encoding| + string = String.new("", encoding: encoding) + exception = assert_raises ArgumentError do + ActiveSupport::Inflector.transliterate(string) + end + assert_equal "Can not transliterate strings with #{encoding} encoding", exception.message + end + end + + def test_transliterate_handles_strings_with_invalid_utf8_bytes + string = String.new("\255", encoding: Encoding::UTF_8) + assert_equal "?", ActiveSupport::Inflector.transliterate(string) + end + + def test_transliterate_handles_strings_with_invalid_us_ascii_bytes + string = String.new("\255", encoding: Encoding::US_ASCII) + assert_equal "?", ActiveSupport::Inflector.transliterate(string) + end + + def test_transliterate_handles_strings_with_invalid_gb18030_bytes + string = String.new("\255", encoding: Encoding::GB18030) + assert_equal "?", ActiveSupport::Inflector.transliterate(string) + end end diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index f8367283fc..a5d097637e 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -34,7 +34,7 @@ Controller Naming Convention The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on. -Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep URL and path helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. +Following this convention will allow you to use the default route generators (e.g. `resources`, etc) without needing to qualify each `:path` or `:controller`, and will keep named route helpers' usage consistent throughout your application. See [Layouts & Rendering Guide](layouts_and_rendering.html) for more details. NOTE: The controller naming convention differs from the naming convention of models, which are expected to be named in singular form. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 60d0de17bc..4acc25bdc2 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -368,7 +368,7 @@ irb(main):001:0> Inside the `rails console` you have access to the `app` and `helper` instances. -With the `app` method you can access URL and path helpers, as well as do requests. +With the `app` method you can access named route helpers, as well as do requests. ```bash >> app.root_path diff --git a/guides/source/engines.md b/guides/source/engines.md index 8961a079b5..b3ac243af9 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -264,7 +264,7 @@ contains a file called `application_helper.rb`. This file will provide any common functionality for the helpers of the engine. The `blorgh` directory is where the other helpers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with -identically-named helpers within other engines or even within the +identically-named route helpers within other engines or even within the application. Within the `app/jobs` directory there is a `blorgh` directory that diff --git a/guides/source/routing.md b/guides/source/routing.md index 4aeb9ee585..161984c993 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -210,7 +210,7 @@ end This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create: -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | ------------------------ | ---------------------- | ---------------------------- | | GET | /admin/articles | admin/articles#index | admin_articles_path | | GET | /admin/articles/new | admin/articles#new | new_admin_article_path | @@ -250,7 +250,7 @@ resources :articles, path: '/admin/articles' In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `ArticlesController`: -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | ------------------------ | -------------------- | ---------------------- | | GET | /admin/articles | articles#index | articles_path | | GET | /admin/articles/new | articles#new | new_article_path | @@ -373,7 +373,7 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | -------------------------------------------- | ----------------- | ------------------------ | | GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | | POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | @@ -383,7 +383,7 @@ The comments resource here will have the following routes generated for it: | PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path | | DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path | -The `:shallow_prefix` option adds the specified parameter to the named helpers: +The `:shallow_prefix` option adds the specified parameter to the named route helpers: ```ruby scope shallow_prefix: "sekret" do @@ -395,7 +395,7 @@ end The comments resource here will have the following routes generated for it: -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | -------------------------------------------- | ----------------- | --------------------------- | | GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path | | POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path | @@ -638,7 +638,7 @@ You can specify a name for any route using the `:as` option: get 'exit', to: 'sessions#destroy', as: :logout ``` -This will create `logout_path` and `logout_url` as named helpers in your application. Calling `logout_path` will return `/exit` +This will create `logout_path` and `logout_url` as named route helpers in your application. Calling `logout_path` will return `/exit` You can also use this to override routing methods defined by resources, like this: @@ -934,7 +934,7 @@ resources :photos, controller: 'images' will recognize incoming paths beginning with `/photos` but route to the `Images` controller: -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | ---------------- | ----------------- | -------------------- | | GET | /photos | images#index | photos_path | | GET | /photos/new | images#new | new_photo_path | @@ -982,7 +982,7 @@ NOTE: Of course, you can use the more advanced constraints available in non-reso TIP: By default the `:id` parameter doesn't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within an `:id` add a constraint which overrides this - for example `id: /[^\/]+/` allows anything except a slash. -### Overriding the Named Helpers +### Overriding the Named Route Helpers The `:as` option lets you override the normal naming for the named route helpers. For example: @@ -992,7 +992,7 @@ resources :photos, as: 'images' will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the `:as` option to name the helpers. -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | ---------------- | ----------------- | -------------------- | | GET | /photos | photos#index | images_path | | GET | /photos/new | photos#new | new_image_path | @@ -1097,7 +1097,7 @@ end Rails now creates routes to the `CategoriesController`. -| HTTP Verb | Path | Controller#Action | Named Helper | +| HTTP Verb | Path | Controller#Action | Named Route Helper | | --------- | -------------------------- | ------------------ | ----------------------- | | GET | /kategorien | categories#index | categories_path | | GET | /kategorien/neu | categories#new | new_category_path | diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md index 8cf8efefd0..b740e933ba 100644 --- a/guides/source/working_with_javascript_in_rails.md +++ b/guides/source/working_with_javascript_in_rails.md @@ -14,6 +14,7 @@ After reading this guide, you will know: * How Rails' built-in helpers assist you. * How to handle Ajax on the server side. * The Turbolinks gem. +* How to include your Cross-Site Request Forgery token in request headers ------------------------------------------------------------------------------- @@ -524,6 +525,23 @@ For more details, including other events you can bind to, check out [the Turbolinks README](https://github.com/turbolinks/turbolinks/blob/master/README.md). +Cross-Site Request Forgery (CSRF) token in Ajax +---- + +When using another library to make Ajax calls, it is necessary to add +the security token as a default header for Ajax calls in your library. To get +the token: + +```javascript +var token = document.getElementsByName('csrf-token')[0].content +``` + +You can then submit this token as a X-CSRF-Token in your header for your +Ajax requst. You do not need to add a CSRF for GET requests, only non-GET +requests. + +You can read more about about Cross-Site Request Forgery in [Security](https://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf) + Other Resources --------------- diff --git a/railties/lib/rails/command/helpers/pretty_credentials.rb b/railties/lib/rails/command/helpers/pretty_credentials.rb new file mode 100644 index 0000000000..873ed0e825 --- /dev/null +++ b/railties/lib/rails/command/helpers/pretty_credentials.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "fileutils" + +module Rails + module Command + module Helpers + module PrettyCredentials + Error = Class.new(StandardError) + + def opt_in_pretty_credentials + unless already_answered? || already_opted_in? + answer = yes?("Would you like to make the credentials diff from git more readable in the future? [Y/n]") + end + + opt_in! if answer + FileUtils.touch(tracker) unless answer.nil? + rescue Error + say("Couldn't setup git to prettify the credentials diff") + end + + private + def already_answered? + tracker.exist? + end + + def already_opted_in? + system_call("git config --get 'diff.rails_credentials.textconv'", accepted_codes: [0, 1]) + end + + def opt_in! + system_call("git config diff.rails_credentials.textconv 'bin/rails credentials:show'", accepted_codes: [0]) + + git_attributes = Rails.root.join(".gitattributes") + File.open(git_attributes, "a+") do |file| + file.write(<<~EOM) + config/credentials/*.yml.enc diff=rails_credentials + config/credentials.yml.enc diff=rails_credentials + EOM + end + end + + def tracker + Rails.root.join("tmp", "rails_pretty_credentials") + end + + def system_call(command_line, accepted_codes:) + result = system(command_line) + raise(Error) if accepted_codes.exclude?($?.exitstatus) + result + end + end + end + end +end diff --git a/railties/lib/rails/commands/credentials/credentials_command.rb b/railties/lib/rails/commands/credentials/credentials_command.rb index e23a1b3008..772e105007 100644 --- a/railties/lib/rails/commands/credentials/credentials_command.rb +++ b/railties/lib/rails/commands/credentials/credentials_command.rb @@ -2,12 +2,15 @@ require "active_support" require "rails/command/helpers/editor" +require "rails/command/helpers/pretty_credentials" require "rails/command/environment_argument" +require "pathname" module Rails module Command class CredentialsCommand < Rails::Command::Base # :nodoc: include Helpers::Editor + include Helpers::PrettyCredentials include EnvironmentArgument self.environment_desc = "Uses credentials from config/credentials/:environment.yml.enc encrypted by config/credentials/:environment.key key" @@ -34,20 +37,29 @@ module Rails end say "File encrypted and saved." + opt_in_pretty_credentials rescue ActiveSupport::MessageEncryptor::InvalidMessage say "Couldn't decrypt #{content_path}. Perhaps you passed the wrong key?" end - def show - extract_environment_option_from_argument(default_environment: nil) + def show(git_textconv_path = nil) + if git_textconv_path + default_environment = extract_environment_from_path(git_textconv_path) + fallback_message = File.read(git_textconv_path) + end + + extract_environment_option_from_argument(default_environment: default_environment) require_application! - say credentials.read.presence || missing_credentials_message + say credentials(git_textconv_path).read.presence || fallback_message || missing_credentials_message + rescue => e + raise(e) unless git_textconv_path + fallback_message end private - def credentials - Rails.application.encrypted(content_path, key_path: key_path) + def credentials(content = nil) + Rails.application.encrypted(content || content_path, key_path: key_path) end def ensure_encryption_key_has_been_added @@ -77,7 +89,6 @@ module Rails end end - def content_path options[:environment] ? "config/credentials/#{options[:environment]}.yml.enc" : "config/credentials.yml.enc" end @@ -86,6 +97,17 @@ module Rails options[:environment] ? "config/credentials/#{options[:environment]}.key" : "config/master.key" end + def extract_environment_from_path(path) + regex = %r{ + ([A-Za-z0-9]+) # match the environment + (?<!credentials) # don't match if file contains the word "credentials" + # in such case, the environment should be the default one + \.yml\.enc # look for `.yml.enc` file extension + }x + path.match(regex) + + Regexp.last_match(1) + end def encryption_key_file_generator require "rails/generators" diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index fe56e3d076..e8456e8c19 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -162,7 +162,6 @@ module ApplicationTests rails "generate", "scaffold", "user", "username:string", "password:string" with_rails_env("test") do rails("db:migrate") - rails("webpacker:compile") end output = rails("test") @@ -194,7 +193,6 @@ module ApplicationTests rails "generate", "scaffold", "LineItems", "product:references", "cart:belongs_to" with_rails_env("test") do rails("db:migrate") - rails("webpacker:compile") end output = rails("test") diff --git a/railties/test/commands/credentials_test.rb b/railties/test/commands/credentials_test.rb index 0ee36081c0..562e2ec382 100644 --- a/railties/test/commands/credentials_test.rb +++ b/railties/test/commands/credentials_test.rb @@ -4,6 +4,7 @@ require "isolation/abstract_unit" require "env_helpers" require "rails/command" require "rails/commands/credentials/credentials_command" +require "fileutils" class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation, EnvHelpers @@ -88,10 +89,107 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase assert_match(/secret_key_base/, output) end + test "edit ask the user to opt in to pretty credentials" do + assert_match(/Would you like to make the credentials diff from git/, run_edit_command) + end + + test "edit doesn't ask the user to opt in to pretty credentials when alreasy asked" do + app_file("tmp/rails_pretty_credentials", "") + + assert_no_match(/Would you like to make the credentials diff from git/, run_edit_command) + end + + test "edit doesn't ask the user to opt in when user already opted in" do + content = <<~EOM + [diff "rails_credentials"] + textconv = bin/rails credentials:show + EOM + app_file(".git/config", content) + + assert_no_match(/Would you like to make the credentials diff from git/, run_edit_command) + end + + test "edit ask the user to opt in to pretty credentials, user accepts" do + file = File.open("foo", "w") + file.write("y") + file.rewind + + run_edit_command(stdin: file.path) + + git_attributes = app_path(".gitattributes") + expected = <<~EOM + config/credentials/*.yml.enc diff=rails_credentials + config/credentials.yml.enc diff=rails_credentials + EOM + assert(File.exist?(git_attributes)) + assert_equal(expected, File.read(git_attributes)) + Dir.chdir(app_path) do + assert_equal("bin/rails credentials:show\n", `git config --get 'diff.rails_credentials.textconv'`) + end + ensure + File.delete(file) + end + + test "edit ask the user to opt in to pretty credentials, user refuses" do + file = File.open("foo", "w") + file.write("n") + file.rewind + + run_edit_command(stdin: file.path) + + git_attributes = app_path(".gitattributes") + assert_not(File.exist?(git_attributes)) + ensure + File.delete(file) + end + test "show credentials" do assert_match(/access_key_id: 123/, run_show_command) end + test "show command when argument is provided (from git diff left file)" do + run_edit_command(environment: "development") + + assert_match(/access_key_id: 123/, run_show_command("config/credentials/development.yml.enc")) + end + + test "show command when argument is provided (from git diff right file)" do + run_edit_command(environment: "development") + + dir = Dir.mktmpdir + file_path = File.join(dir, "KnAM4a_development.yml.enc") + file_content = File.read(app_path("config", "credentials", "development.yml.enc")) + File.write(file_path, file_content) + + assert_match(/access_key_id: 123/, run_show_command(file_path)) + ensure + FileUtils.rm_rf(dir) + end + + test "show command when argument is provided (git diff) and filename is the master credentials" do + assert_match(/access_key_id: 123/, run_show_command("config/credentials.yml.enc")) + end + + test "show command when argument is provided (git diff) and master key is not available" do + remove_file "config/master.key" + + raw_content = File.read(app_path("config", "credentials.yml.enc")) + assert_match(raw_content, run_show_command("config/credentials.yml.enc")) + end + + test "show command when argument is provided (git diff) return the raw encrypted content in an error occurs" do + run_edit_command(environment: "development") + + dir = Dir.mktmpdir + file_path = File.join(dir, "20190807development.yml.enc") + file_content = File.read(app_path("config", "credentials", "development.yml.enc")) + File.write(file_path, file_content) + + assert_match(file_content, run_show_command(file_path)) + ensure + FileUtils.rm_rf(dir) + end + test "show command raises error when require_master_key is specified and key does not exist" do remove_file "config/master.key" add_to_config "config.require_master_key = true" @@ -128,8 +226,9 @@ class Rails::Command::CredentialsCommandTest < ActiveSupport::TestCase end end - def run_show_command(environment: nil, **options) + def run_show_command(path = nil, environment: nil, **options) args = environment ? ["--environment", environment] : [] + args.unshift(path) rails "credentials:show", args, **options end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 0fe62df8ba..6077ba3ee7 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -301,7 +301,7 @@ module TestHelpers # stderr:: true to pass STDERR output straight to the "real" STDERR. # By default, the STDERR and STDOUT of the process will be # combined in the returned string. - def rails(*args, allow_failure: false, stderr: false) + def rails(*args, allow_failure: false, stderr: false, stdin: File::NULL) args = args.flatten fork = true @@ -328,7 +328,7 @@ module TestHelpers out_read.close err_read.close if err_read - $stdin.reopen(File::NULL, "r") + $stdin.reopen(stdin, "r") $stdout.reopen(out_write) $stderr.reopen(err_write) |