diff options
Diffstat (limited to 'actionpack/lib')
35 files changed, 466 insertions, 92 deletions
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index bd19b8cd5d..f43784f9f2 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -22,6 +22,7 @@ module ActionController autoload_under "metal" do autoload :ConditionalGet + autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming autoload :EtagWithTemplateDigest diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index b73269871b..204a3d400c 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -225,6 +225,7 @@ module ActionController Flash, FormBuilder, RequestForgeryProtection, + ContentSecurityPolicy, ForceSSL, Streaming, DataStreaming, diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb index 457884ea08..f875aa5e6b 100644 --- a/actionpack/lib/action_controller/metal.rb +++ b/actionpack/lib/action_controller/metal.rb @@ -230,18 +230,16 @@ module ActionController # Returns a Rack endpoint for the given action name. def self.action(name) + app = lambda { |env| + req = ActionDispatch::Request.new(env) + res = make_response! req + new.dispatch(name, req, res) + } + if middleware_stack.any? - middleware_stack.build(name) do |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - end + middleware_stack.build(name, app) else - lambda { |env| - req = ActionDispatch::Request.new(env) - res = make_response! req - new.dispatch(name, req, res) - } + app end end diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb new file mode 100644 index 0000000000..48a7109bea --- /dev/null +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module ActionController #:nodoc: + module ContentSecurityPolicy + # TODO: Documentation + extend ActiveSupport::Concern + + module ClassMethods + def content_security_policy(**options, &block) + before_action(options) do + if block_given? + policy = request.content_security_policy.clone + yield policy + request.content_security_policy = policy + end + end + end + + def content_security_policy_report_only(report_only = true, **options) + before_action(options) do + request.content_security_policy_report_only = report_only + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 0ba1f9f783..7de500d119 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -39,7 +39,7 @@ module ActionController # end # # ==== URL Options - # You can pass any of the following options to affect the redirect url + # You can pass any of the following options to affect the redirect URL # * <tt>host</tt> - Redirect to a different host name # * <tt>subdomain</tt> - Redirect to a different subdomain # * <tt>domain</tt> - Redirect to a different domain @@ -73,7 +73,7 @@ module ActionController # Redirect the existing request to use the HTTPS protocol. # # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the url and + # * <tt>host_or_options</tt> - Either a host name or any of the URL and # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 0c8132684a..01676f3237 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -72,10 +72,10 @@ module ActionController before_action(options.except(:name, :password, :realm)) do authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password| # This comparison uses & so that it doesn't short circuit and - # uses `variable_size_secure_compare` so that length information + # uses `secure_compare` so that length information # isn't leaked. - ActiveSupport::SecurityUtils.variable_size_secure_compare(name, options[:name]) & - ActiveSupport::SecurityUtils.variable_size_secure_compare(password, options[:password]) + ActiveSupport::SecurityUtils.secure_compare(name, options[:name]) & + ActiveSupport::SecurityUtils.secure_compare(password, options[:password]) end end end @@ -350,10 +350,7 @@ module ActionController # authenticate_or_request_with_http_token do |token, options| # # Compare the tokens in a time-constant manner, to mitigate # # timing attacks. - # ActiveSupport::SecurityUtils.secure_compare( - # ::Digest::SHA256.hexdigest(token), - # ::Digest::SHA256.hexdigest(TOKEN) - # ) + # ActiveSupport::SecurityUtils.secure_compare(token, TOKEN) # end # end # end diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index 8de57f9199..4c2b5120eb 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -68,7 +68,7 @@ module ActionController # if possible, otherwise redirects to the provided default fallback # location. # - # The referrer information is pulled from the HTTP `Referer` (sic) header on + # The referrer information is pulled from the HTTP +Referer+ (sic) header on # the request. This is an optional header and its presence on the request is # subject to browser security settings and user preferences. If the request # is missing this header, the <tt>fallback_location</tt> will be used. @@ -82,8 +82,8 @@ module ActionController # redirect_back fallback_location: '/', allow_other_host: false # # ==== Options - # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing `Referer` header. - # * <tt>:allow_other_host</tt> - Allows or disallow redirection to the host that is different to the current host + # * <tt>:fallback_location</tt> - The default fallback location that will be used on missing +Referer+ header. + # * <tt>:allow_other_host</tt> - Allow or disallow redirection to the host that is different to the current host, defaults to true. # # All other options that can be passed to <tt>redirect_to</tt> are accepted as # options and the behavior is identical. diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index bd133f24a1..0ab313e398 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -3,6 +3,7 @@ require "rack/session/abstract/id" require "action_controller/metal/exceptions" require "active_support/security_utils" +require "active_support/core_ext/string/strip" module ActionController #:nodoc: class InvalidAuthenticityToken < ActionControllerError #:nodoc: @@ -216,7 +217,7 @@ module ActionController #:nodoc: # The actual before_action that is used to verify the CSRF token. # Don't override this directly. Provide your own forgery protection # strategy instead. If you override, you'll disable same-origin - # `<script>` verification. + # <tt><script></tt> verification. # # Lean on the protect_from_forgery declaration to mark which actions are # due for same-origin request verification. If protect_from_forgery is @@ -250,7 +251,7 @@ module ActionController #:nodoc: private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING # :startdoc: - # If `verify_authenticity_token` was run (indicating that we have + # If +verify_authenticity_token+ was run (indicating that we have # forgery protection enabled for this request) then also verify that # we aren't serving an unauthorized cross-origin response. def verify_same_origin_request # :doc: @@ -267,7 +268,7 @@ module ActionController #:nodoc: @marked_for_same_origin_verification = request.get? end - # If the `verify_authenticity_token` before_action ran, verify that + # If the +verify_authenticity_token+ before_action ran, verify that # JavaScript responses are only served to same-origin GET requests. def marked_for_same_origin_verification? # :doc: @marked_for_same_origin_verification ||= false @@ -369,7 +370,7 @@ module ActionController #:nodoc: end def compare_with_real_token(token, session) # :doc: - ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session)) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, real_csrf_token(session)) end def valid_per_form_csrf_token?(token, session) # :doc: @@ -380,7 +381,7 @@ module ActionController #:nodoc: request.request_method ) - ActiveSupport::SecurityUtils.secure_compare(token, correct_token) + ActiveSupport::SecurityUtils.fixed_length_secure_compare(token, correct_token) else false end @@ -415,11 +416,21 @@ module ActionController #:nodoc: allow_forgery_protection end + NULL_ORIGIN_MESSAGE = <<-MSG.strip_heredoc + The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually + means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the + best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. + If you cannot change the referrer policy, you can disable origin checking with the + Rails.application.config.action_controller.forgery_protection_origin_check setting. + MSG + # Checks if the request originated from the same origin by looking at the # Origin header. def valid_request_origin? # :doc: if forgery_protection_origin_check # We accept blank origin headers because some user agents don't send it. + raise InvalidAuthenticityToken, NULL_ORIGIN_MESSAGE if request.origin == "null" request.origin.nil? || request.origin == request.base_url else true diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb index 0b1598bf1b..8dc01a5eb9 100644 --- a/actionpack/lib/action_controller/metal/streaming.rb +++ b/actionpack/lib/action_controller/metal/streaming.rb @@ -183,7 +183,7 @@ module ActionController #:nodoc: # unicorn_rails --config-file unicorn.config.rb # # You may also want to configure other parameters like <tt>:tcp_nodelay</tt>. - # Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen + # Please check its documentation for more information: https://bogomips.org/unicorn/Unicorn/Configurator.html#method-i-listen # # If you are using Unicorn with NGINX, you may need to tweak NGINX. # Streaming should work out of the box on Rainbows. diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index ef7c4c4c16..a56ac749f8 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -335,7 +335,7 @@ module ActionController # the same way as <tt>Hash#each_pair</tt>. def each_pair(&block) @parameters.each_pair do |key, value| - yield key, convert_hashes_to_parameters(key, value) + yield [key, convert_hashes_to_parameters(key, value)] end end alias_method :each, :each_pair diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 4b408750a4..798d142755 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -256,7 +256,7 @@ module ActionController # # def test_create # json = {book: { title: "Love Hina" }}.to_json - # post :create, json + # post :create, body: json # end # # == Special instance variables diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 34937f3229..0822cdc0a6 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -42,6 +42,7 @@ module ActionDispatch eager_autoload do autoload_under "http" do + autoload :ContentSecurityPolicy autoload :Request autoload :Response end diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb index 3328ce17a0..a8febc32b3 100644 --- a/actionpack/lib/action_dispatch/http/cache.rb +++ b/actionpack/lib/action_dispatch/http/cache.rb @@ -133,7 +133,7 @@ module ActionDispatch end def generate_strong_etag(validators) - %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") + %("#{ActiveSupport::Digest.hexdigest(ActiveSupport::Cache.expand_cache_key(validators))}") end def cache_control_segments diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb new file mode 100644 index 0000000000..4883e23d24 --- /dev/null +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -0,0 +1,231 @@ +# frozen_string_literal: true + +require "active_support/core_ext/object/deep_dup" + +module ActionDispatch #:nodoc: + class ContentSecurityPolicy + class Middleware + CONTENT_TYPE = "Content-Type".freeze + POLICY = "Content-Security-Policy".freeze + POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only".freeze + + def initialize(app) + @app = app + end + + def call(env) + request = ActionDispatch::Request.new env + _, headers, _ = response = @app.call(env) + + return response unless html_response?(headers) + return response if policy_present?(headers) + + if policy = request.content_security_policy + headers[header_name(request)] = policy.build(request.controller_instance) + end + + response + end + + private + + def html_response?(headers) + if content_type = headers[CONTENT_TYPE] + content_type =~ /html/ + end + end + + def header_name(request) + if request.content_security_policy_report_only + POLICY_REPORT_ONLY + else + POLICY + end + end + + def policy_present?(headers) + headers[POLICY] || headers[POLICY_REPORT_ONLY] + end + end + + module Request + POLICY = "action_dispatch.content_security_policy".freeze + POLICY_REPORT_ONLY = "action_dispatch.content_security_policy_report_only".freeze + + def content_security_policy + get_header(POLICY) + end + + def content_security_policy=(policy) + set_header(POLICY, policy) + end + + def content_security_policy_report_only + get_header(POLICY_REPORT_ONLY) + end + + def content_security_policy_report_only=(value) + set_header(POLICY_REPORT_ONLY, value) + end + end + + MAPPINGS = { + self: "'self'", + unsafe_eval: "'unsafe-eval'", + unsafe_inline: "'unsafe-inline'", + none: "'none'", + http: "http:", + https: "https:", + data: "data:", + mediastream: "mediastream:", + blob: "blob:", + filesystem: "filesystem:", + report_sample: "'report-sample'", + strict_dynamic: "'strict-dynamic'" + }.freeze + + DIRECTIVES = { + base_uri: "base-uri", + child_src: "child-src", + connect_src: "connect-src", + default_src: "default-src", + font_src: "font-src", + form_action: "form-action", + frame_ancestors: "frame-ancestors", + frame_src: "frame-src", + img_src: "img-src", + manifest_src: "manifest-src", + media_src: "media-src", + object_src: "object-src", + script_src: "script-src", + style_src: "style-src", + worker_src: "worker-src" + }.freeze + + private_constant :MAPPINGS, :DIRECTIVES + + attr_reader :directives + + def initialize + @directives = {} + yield self if block_given? + end + + def initialize_copy(other) + @directives = other.directives.deep_dup + end + + DIRECTIVES.each do |name, directive| + define_method(name) do |*sources| + if sources.first + @directives[directive] = apply_mappings(sources) + else + @directives.delete(directive) + end + end + end + + def block_all_mixed_content(enabled = true) + if enabled + @directives["block-all-mixed-content"] = true + else + @directives.delete("block-all-mixed-content") + end + end + + def plugin_types(*types) + if types.first + @directives["plugin-types"] = types + else + @directives.delete("plugin-types") + end + end + + def report_uri(uri) + @directives["report-uri"] = [uri] + end + + def require_sri_for(*types) + if types.first + @directives["require-sri-for"] = types + else + @directives.delete("require-sri-for") + end + end + + def sandbox(*values) + if values.empty? + @directives["sandbox"] = true + elsif values.first + @directives["sandbox"] = values + else + @directives.delete("sandbox") + end + end + + def upgrade_insecure_requests(enabled = true) + if enabled + @directives["upgrade-insecure-requests"] = true + else + @directives.delete("upgrade-insecure-requests") + end + end + + def build(context = nil) + build_directives(context).compact.join("; ") + ";" + end + + private + def apply_mappings(sources) + sources.map do |source| + case source + when Symbol + apply_mapping(source) + when String, Proc + source + else + raise ArgumentError, "Invalid content security policy source: #{source.inspect}" + end + end + end + + def apply_mapping(source) + MAPPINGS.fetch(source) do + raise ArgumentError, "Unknown content security policy source mapping: #{source.inspect}" + end + end + + def build_directives(context) + @directives.map do |directive, sources| + if sources.is_a?(Array) + "#{directive} #{build_directive(sources, context).join(' ')}" + elsif sources + directive + else + nil + end + end + end + + def build_directive(sources, context) + sources.map { |source| resolve_source(source, context) } + end + + def resolve_source(source, context) + case source + when String + source + when Symbol + source.to_s + when Proc + if context.nil? + raise RuntimeError, "Missing context for the dynamic content security policy source: #{source.inspect}" + else + context.instance_exec(&source) + end + else + raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" + end + end + end +end diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb index f8e6fca36d..342e6de312 100644 --- a/actionpack/lib/action_dispatch/http/mime_types.rb +++ b/actionpack/lib/action_dispatch/http/mime_types.rb @@ -10,6 +10,7 @@ Mime::Type.register "text/css", :css Mime::Type.register "text/calendar", :ics Mime::Type.register "text/csv", :csv Mime::Type.register "text/vcard", :vcf +Mime::Type.register "text/vtt", :vtt, %w(vtt) Mime::Type.register "image/png", :png, [], %w(png) Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg) @@ -20,6 +21,18 @@ Mime::Type.register "image/svg+xml", :svg Mime::Type.register "video/mpeg", :mpeg, [], %w(mpg mpeg mpe) +Mime::Type.register "audio/mpeg", :mp3, [], %w(mp1 mp2 mp3) +Mime::Type.register "audio/ogg", :ogg, [], %w(oga ogg spx opus) +Mime::Type.register "audio/aac", :m4a, %w( audio/mp4 ), %w(m4a mpg4 aac) + +Mime::Type.register "video/webm", :webm, [], %w(webm) +Mime::Type.register "video/mp4", :mp4, [], %w(mp4 m4v) + +Mime::Type.register "font/otf", :otf, [], %w(otf) +Mime::Type.register "font/ttf", :ttf, [], %w(ttf) +Mime::Type.register "font/woff", :woff, [], %w(woff) +Mime::Type.register "font/woff2", :woff2, [], %w(woff2) + Mime::Type.register "application/xml", :xml, %w( text/xml application/x-xml ) Mime::Type.register "application/rss+xml", :rss Mime::Type.register "application/atom+xml", :atom diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index d631281e4b..3838b84a7a 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -22,6 +22,7 @@ module ActionDispatch include ActionDispatch::Http::Parameters include ActionDispatch::Http::FilterParameters include ActionDispatch::Http::URL + include ActionDispatch::ContentSecurityPolicy::Request include Rack::Request::Env autoload :Session, "action_dispatch/request/session" diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index f0344fd927..35ba44005a 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -274,7 +274,7 @@ module ActionDispatch def standard_port case protocol when "https://" then 443 - else 80 + else 80 end end diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb index bdb78d8d48..56e9e3c83d 100644 --- a/actionpack/lib/action_dispatch/journey/nfa/dot.rb +++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb @@ -9,16 +9,16 @@ module ActionDispatch " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];" } - #memo_nodes = memos.values.flatten.map { |n| - # label = n - # if Journey::Route === n - # label = "#{n.verb.source} #{n.path.spec}" - # end - # " #{n.object_id} [label=\"#{label}\", shape=box];" - #} - #memo_edges = memos.flat_map { |k, memos| - # (memos || []).map { |v| " #{k} -> #{v.object_id};" } - #}.uniq + # memo_nodes = memos.values.flatten.map { |n| + # label = n + # if Journey::Route === n + # label = "#{n.verb.source} #{n.path.spec}" + # end + # " #{n.object_id} [label=\"#{label}\", shape=box];" + # } + # memo_edges = memos.flat_map { |k, memos| + # (memos || []).map { |v| " #{k} -> #{v.object_id};" } + # }.uniq <<-eodot digraph nfa { diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 86a070c6ad..c45d947904 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -161,7 +161,7 @@ module ActionDispatch # # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. - # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. @@ -338,6 +338,9 @@ module ActionDispatch end alias :has_key? :key? + # Returns the cookies as Hash. + alias :to_hash :to_h + def update(other_hash) @cookies.update other_hash.stringify_keys self diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb index 4f69abfa6f..d1b4508378 100644 --- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb +++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb @@ -25,6 +25,7 @@ module ActionDispatch "ActionView::MissingTemplate" => "missing_template", "ActionController::RoutingError" => "routing_error", "AbstractController::ActionNotFound" => "unknown_action", + "ActiveRecord::StatementInvalid" => "invalid_statement", "ActionView::Template::Error" => "template_error" ) diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb index 805d3f2148..da2871b551 100644 --- a/actionpack/lib/action_dispatch/middleware/request_id.rb +++ b/actionpack/lib/action_dispatch/middleware/request_id.rb @@ -30,7 +30,7 @@ module ActionDispatch private def make_request_id(request_id) if request_id.presence - request_id.gsub(/[^\w\-]/, "".freeze).first(255) + request_id.gsub(/[^\w\-@]/, "".freeze).first(255) else internal_request_id end diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb new file mode 100644 index 0000000000..e1b129ccc5 --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb @@ -0,0 +1,21 @@ +<header> + <h1> + <%= @exception.class.to_s %> + <% if @request.parameters['controller'] %> + in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> + <% end %> + </h1> +</header> + +<div id="container"> + <h2> + <%= h @exception.message %> + <% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> + <br />To resolve this issue run: bin/rails active_storage:install + <% end %> + </h2> + + <%= render template: "rescues/_source" %> + <%= render template: "rescues/_trace" %> + <%= render template: "rescues/_request_and_response" %> +</div> diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb new file mode 100644 index 0000000000..033518cf8a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb @@ -0,0 +1,13 @@ +<%= @exception.class.to_s %><% + if @request.parameters['controller'] +%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %> +<% end %> + +<%= @exception.message %> +<% if @exception.message.match? %r{#{ActiveStorage::Blob.table_name}|#{ActiveStorage::Attachment.table_name}} %> +To resolve this issue run: bin/rails active_storage:install +<% end %> + +<%= render template: "rescues/_source" %> +<%= render template: "rescues/_trace" %> +<%= render template: "rescues/_request_and_response" %> diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 855f2ffa47..eb6fbca6ba 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -26,7 +26,10 @@ module ActionDispatch config.action_dispatch.default_headers = { "X-Frame-Options" => "SAMEORIGIN", "X-XSS-Protection" => "1; mode=block", - "X-Content-Type-Options" => "nosniff" + "X-Content-Type-Options" => "nosniff", + "X-Download-Options" => "noopen", + "X-Permitted-Cross-Domain-Policies" => "none", + "Referrer-Policy" => "strict-origin-when-cross-origin" } config.action_dispatch.cookies_rotations = ActiveSupport::Messages::RotationConfiguration.new diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index d86d0b10c2..000847e193 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -130,6 +130,7 @@ module ActionDispatch load_for_read! @delegate.dup.delete_if { |_, v| v.nil? } end + alias :to_h :to_hash # Updates the session with given Hash. # diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index ded42adee9..31eb6104fe 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1573,7 +1573,7 @@ module ActionDispatch # Matches a URL pattern to one or more routes. # For more information, see match[rdoc-ref:Base#match]. # - # match 'path' => 'controller#action', via: patch + # match 'path' => 'controller#action', via: :patch # match 'path', to: 'controller#action', via: :post # match 'path', 'otherpath', on: :member, via: :get def match(path, *rest, &block) @@ -2046,7 +2046,7 @@ module ActionDispatch end module CustomUrls - # Define custom url helpers that will be added to the application's + # Define custom URL helpers that will be added to the application's # routes. This allows you to override and/or replace the default behavior # of routing helpers, e.g: # @@ -2066,11 +2066,11 @@ module ActionDispatch # arguments for +url_for+ which will actually build the URL string. This can # be one of the following: # - # * A string, which is treated as a generated URL - # * A hash, e.g. { controller: "pages", action: "index" } - # * An array, which is passed to `polymorphic_url` - # * An Active Model instance - # * An Active Model class + # * A string, which is treated as a generated URL + # * A hash, e.g. <tt>{ controller: "pages", action: "index" }</tt> + # * An array, which is passed to +polymorphic_url+ + # * An Active Model instance + # * An Active Model class # # NOTE: Other URL helpers can be called in the block but be careful not to invoke # your custom URL helper again otherwise it will result in a stack overflow error. @@ -2082,9 +2082,9 @@ module ActionDispatch # [ :products, options.merge(params.permit(:page, :size).to_h.symbolize_keys) ] # end # - # In this instance the +params+ object comes from the context in which the the + # In this instance the +params+ object comes from the context in which the # block is executed, e.g. generating a URL inside a controller action or a view. - # If the block is executed where there isn't a params object such as this: + # If the block is executed where there isn't a +params+ object such as this: # # Rails.application.routes.url_helpers.browse_path # diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 987e709f6f..ff6998ae31 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -2,7 +2,6 @@ require "action_dispatch/journey" require "active_support/core_ext/object/to_query" -require "active_support/core_ext/hash/slice" require "active_support/core_ext/module/redefine_method" require "active_support/core_ext/module/remove_method" require "active_support/core_ext/array/extract_options" @@ -199,6 +198,16 @@ module ActionDispatch if args.size == arg_size && !inner_options && optimize_routes_generation?(t) options = t.url_options.merge @options options[:path] = optimized_helper(args) + + original_script_name = options.delete(:original_script_name) + script_name = t._routes.find_script_name(options) + + if original_script_name + script_name = original_script_name + script_name + end + + options[:script_name] = script_name + url_strategy.call options else super @@ -845,7 +854,7 @@ module ActionDispatch recognize_path_with_request(req, path, extras) end - def recognize_path_with_request(req, path, extras) + def recognize_path_with_request(req, path, extras, raise_on_missing: true) @router.recognize(req) do |route, params| params.merge!(extras) params.each do |key, value| @@ -865,12 +874,14 @@ module ActionDispatch return req.path_parameters elsif app.matches?(req) && app.engine? - path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras) - return path_parameters + path_parameters = app.rack_app.routes.recognize_path_with_request(req, path, extras, raise_on_missing: false) + return path_parameters if path_parameters end end - raise ActionController::RoutingError, "No route matches #{path.inspect}" + if raise_on_missing + raise ActionController::RoutingError, "No route matches #{path.inspect}" + end end end # :startdoc: diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 3ae533dd37..fa345dccdf 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -155,7 +155,7 @@ module ActionDispatch # Missing routes keys may be filled in from the current request's parameters # (e.g. +:controller+, +:action+, +:id+ and any other parameters that are # placed in the path). Given that the current action has been reached - # through `GET /users/1`: + # through <tt>GET /users/1</tt>: # # url_for(only_path: true) # => '/users/1' # url_for(only_path: true, action: 'edit') # => '/users/1/edit' diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index 7246e01cff..f85f816bb9 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -6,6 +6,7 @@ require "capybara/dsl" require "capybara/minitest" require "action_controller" require "action_dispatch/system_testing/driver" +require "action_dispatch/system_testing/browser" require "action_dispatch/system_testing/server" require "action_dispatch/system_testing/test_helpers/screenshot_helper" require "action_dispatch/system_testing/test_helpers/setup_and_teardown" @@ -69,6 +70,9 @@ module ActionDispatch # size of the browser screen. These two options are not applicable for # headless drivers and will be silently ignored if passed. # + # Headless browsers such as headless Chrome and headless Firefox are also supported. + # You can use these browsers by setting the +:using+ argument to +:headless_chrome+ or +:headless_firefox+. + # # To use a headless driver, like Poltergeist, update your Gemfile to use # Poltergeist instead of Selenium and then declare the driver name in the # +application_system_test_case.rb+ file. In this case, you would leave out @@ -121,11 +125,15 @@ module ActionDispatch # # driven_by :poltergeist # - # driven_by :selenium, using: :firefox + # driven_by :selenium, screen_size: [800, 800] + # + # driven_by :selenium, using: :chrome # # driven_by :selenium, using: :headless_chrome # - # driven_by :selenium, screen_size: [800, 800] + # driven_by :selenium, using: :firefox + # + # driven_by :selenium, using: :headless_firefox def self.driven_by(driver, using: :chrome, screen_size: [1400, 1400], options: {}) self.driver = SystemTesting::Driver.new(driver, using: using, screen_size: screen_size, options: options) end diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb new file mode 100644 index 0000000000..10e6888ab3 --- /dev/null +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module ActionDispatch + module SystemTesting + class Browser # :nodoc: + attr_reader :name + + def initialize(name) + @name = name + end + + def type + case name + when :headless_chrome + :chrome + when :headless_firefox + :firefox + else + name + end + end + + def options + case name + when :headless_chrome + headless_chrome_browser_options + when :headless_firefox + headless_firefox_browser_options + end + end + + private + def headless_chrome_browser_options + options = Selenium::WebDriver::Chrome::Options.new + options.args << "--headless" + options.args << "--disable-gpu" + + options + end + + def headless_firefox_browser_options + options = Selenium::WebDriver::Firefox::Options.new + options.args << "-headless" + + options + end + end + end +end diff --git a/actionpack/lib/action_dispatch/system_testing/driver.rb b/actionpack/lib/action_dispatch/system_testing/driver.rb index 2687772b4b..5252ff6746 100644 --- a/actionpack/lib/action_dispatch/system_testing/driver.rb +++ b/actionpack/lib/action_dispatch/system_testing/driver.rb @@ -5,7 +5,7 @@ module ActionDispatch class Driver # :nodoc: def initialize(name, **options) @name = name - @browser = options[:using] + @browser = Browser.new(options[:using]) @screen_size = options[:screen_size] @options = options[:options] end @@ -32,23 +32,11 @@ module ActionDispatch end def browser_options - if @browser == :headless_chrome - browser_options = Selenium::WebDriver::Chrome::Options.new - browser_options.args << "--headless" - browser_options.args << "--disable-gpu" - - @options.merge(options: browser_options) - else - @options - end - end - - def browser - @browser == :headless_chrome ? :chrome : @browser + @options.merge(options: @browser.options).compact end def register_selenium(app) - Capybara::Selenium::Driver.new(app, { browser: browser }.merge(browser_options)).tap do |driver| + Capybara::Selenium::Driver.new(app, { browser: @browser.type }.merge(browser_options)).tap do |driver| driver.browser.manage.window.size = Selenium::WebDriver::Dimension.new(*@screen_size) end end diff --git a/actionpack/lib/action_dispatch/system_testing/server.rb b/actionpack/lib/action_dispatch/system_testing/server.rb index 8f1b6725b1..4fc1f33767 100644 --- a/actionpack/lib/action_dispatch/system_testing/server.rb +++ b/actionpack/lib/action_dispatch/system_testing/server.rb @@ -20,7 +20,7 @@ module ActionDispatch end def set_server - Capybara.server = :puma, { Silent: self.class.silence_puma } + Capybara.server = :puma, { Silent: self.class.silence_puma } if Capybara.server == Capybara.servers[:default] end def set_port diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb index 6c337cdc31..df0c5d3f0e 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb @@ -15,12 +15,11 @@ module ActionDispatch # # You can set the +RAILS_SYSTEM_TESTING_SCREENSHOT+ environment variable to # control the output. Possible values are: - # * [+inline+ (default)] display the screenshot in the terminal using the + # * [+simple+ (default)] Only displays the screenshot path. + # This is the default value. + # * [+inline+] Display the screenshot in the terminal using the # iTerm image protocol (https://iterm2.com/documentation-images.html). - # * [+simple+] only display the screenshot path. - # This is the default value if the +CI+ environment variables - # is defined. - # * [+artifact+] display the screenshot in the terminal, using the terminal + # * [+artifact+] Display the screenshot in the terminal, using the terminal # artifact format (https://buildkite.github.io/terminal/inline-images/). def take_screenshot save_image @@ -59,11 +58,8 @@ module ActionDispatch # Environment variables have priority output_type = ENV["RAILS_SYSTEM_TESTING_SCREENSHOT"] || ENV["CAPYBARA_INLINE_SCREENSHOT"] - # If running in a CI environment, default to simple - output_type ||= "simple" if ENV["CI"] - - # Default - output_type ||= "inline" + # Default to outputting a path to the screenshot + output_type ||= "simple" output_type end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index 95fdd3affb..3f69109633 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true #-- -# Copyright (c) 2004-2017 David Heinemeier Hansson +# Copyright (c) 2004-2018 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb index 28bc153f4d..37969fcb57 100644 --- a/actionpack/lib/action_pack/gem_version.rb +++ b/actionpack/lib/action_pack/gem_version.rb @@ -7,8 +7,8 @@ module ActionPack end module VERSION - MAJOR = 5 - MINOR = 2 + MAJOR = 6 + MINOR = 0 TINY = 0 PRE = "alpha" |