diff options
Diffstat (limited to 'actionpack')
-rw-r--r-- | actionpack/CHANGELOG.md | 32 | ||||
-rw-r--r-- | actionpack/lib/action_controller.rb | 1 | ||||
-rw-r--r-- | actionpack/lib/action_controller/base.rb | 1 | ||||
-rw-r--r-- | actionpack/lib/action_controller/metal/etag_with_flash.rb | 16 | ||||
-rw-r--r-- | actionpack/lib/action_controller/metal/implicit_render.rb | 2 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/http/url.rb | 86 | ||||
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/ssl.rb | 10 | ||||
-rw-r--r-- | actionpack/test/controller/flash_test.rb | 29 | ||||
-rw-r--r-- | actionpack/test/dispatch/ssl_test.rb | 14 |
9 files changed, 124 insertions, 67 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 9dab1cc76a..a66a1e8af3 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,35 @@ +* Include the content of the flash in the auto-generated etag. This solves the following problem: + + 1. POST /messages + 2. redirect_to messages_url, notice: 'Message was created' + 3. GET /messages/1 + 4. GET /messages + + Step 4 would before still include the flash message, even though it's no longer relevant, + because the etag cache was recorded with the flash in place and didn't change when it was gone. + + *DHH* + +* SSL: Changes redirect behavior for all non-GET and non-HEAD requests + (like POST/PUT/PATCH etc) to `http://` resources to redirect to `https://` + with a [307 status code](http://tools.ietf.org/html/rfc7231#section-6.4.7) instead of [301 status code](http://tools.ietf.org/html/rfc7231#section-6.4.2). + + 307 status code instructs the HTTP clients to preserve the original + request method while redirecting. It has been part of HTTP RFC since + 1999 and is implemented/recognized by most (if not all) user agents. + + # Before + POST http://example.com/articles (i.e. ArticlesContoller#create) + redirects to + GET https://example.com/articles (i.e. ArticlesContoller#index) + + # After + POST http://example.com/articles (i.e. ArticlesContoller#create) + redirects to + POST https://example.com/articles (i.e. ArticlesContoller#create) + + *Chirag Singhal* + * Add `:as` option to `ActionController:TestCase#process` and related methods. Specifying `as: mime_type` allows the `CONTENT_TYPE` header to be specified diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index fc86a907b3..50f20aa789 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -23,6 +23,7 @@ module ActionController autoload :Cookies autoload :DataStreaming autoload :EtagWithTemplateDigest + autoload :EtagWithFlash autoload :Flash autoload :ForceSSL autoload :Head diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 68a526eccb..ca8066cd82 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -213,6 +213,7 @@ module ActionController Renderers::All, ConditionalGet, EtagWithTemplateDigest, + EtagWithFlash, Caching, MimeResponds, ImplicitRender, diff --git a/actionpack/lib/action_controller/metal/etag_with_flash.rb b/actionpack/lib/action_controller/metal/etag_with_flash.rb new file mode 100644 index 0000000000..474d75f02e --- /dev/null +++ b/actionpack/lib/action_controller/metal/etag_with_flash.rb @@ -0,0 +1,16 @@ +module ActionController + # When you're using the flash, it's generally used as a conditional on the view. + # This means the content of the view depends on the flash. Which in turn means + # that the etag for a response should be computed with the content of the flash + # in mind. This does that by including the content of the flash as a component + # in the etag that's generated for a response. + module EtagWithFlash + extend ActiveSupport::Concern + + include ActionController::ConditionalGet + + included do + etag { flash unless flash.empty? } + end + end +end diff --git a/actionpack/lib/action_controller/metal/implicit_render.rb b/actionpack/lib/action_controller/metal/implicit_render.rb index c414527d63..4137db99ea 100644 --- a/actionpack/lib/action_controller/metal/implicit_render.rb +++ b/actionpack/lib/action_controller/metal/implicit_render.rb @@ -48,7 +48,7 @@ module ActionController "NOTE! For XHR/Ajax or API requests, this action would normally " \ "respond with 204 No Content: an empty white screen. Since you're " \ "loading it in a web browser, we assume that you expected to " \ - "actually render a template, not… nothing, so we're showing an " \ + "actually render a template, not nothing, so we're showing an " \ "error to be extra-clear. If you expect 204 No Content, carry on. " \ "That's what you'll get from an XHR or API request. Give it a shot." diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index e85ea90b94..06ffa983d1 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -192,11 +192,7 @@ module ActionDispatch # Returns the complete URL used for this request. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req.url # => "http://example.com" def url protocol + host_with_port + fullpath @@ -204,14 +200,10 @@ module ActionDispatch # Returns 'https://' if this is an SSL request and 'http://' otherwise. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req.protocol # => "http://" # - # req = Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com', 'HTTPS' => 'on' # req.protocol # => "https://" def protocol @protocol ||= ssl? ? "https://" : "http://" @@ -219,17 +211,13 @@ module ActionDispatch # Returns the \host and port for this request, such as "example.com:8080". # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req.raw_host_with_port # => "example.com" # - # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req.raw_host_with_port # => "example.com:80" # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.raw_host_with_port # => "example.com:8080" def raw_host_with_port if forwarded = x_forwarded_host.presence @@ -241,11 +229,7 @@ module ActionDispatch # Returns the host for this request, such as "example.com". # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.host # => "example.com" def host raw_host_with_port.sub(/:\d+$/, "".freeze) @@ -255,17 +239,13 @@ module ActionDispatch # "example.com:8080". Port is only included if it is not a default port # (80 or 443) # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req.host_with_port # => "example.com" # - # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req.host_with_port # => "example.com" # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.host_with_port # => "example.com:8080" def host_with_port "#{host}#{port_string}" @@ -273,14 +253,10 @@ module ActionDispatch # Returns the port number of this request as an integer. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com' # req.port # => 80 # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.port # => 8080 def port @port ||= begin @@ -294,11 +270,7 @@ module ActionDispatch # Returns the standard \port number for this request's protocol. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.standard_port # => 80 def standard_port case protocol @@ -309,14 +281,10 @@ module ActionDispatch # Returns whether this request is using the standard port # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req.standard_port? # => true # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.standard_port? # => false def standard_port? port == standard_port @@ -325,14 +293,10 @@ module ActionDispatch # Returns a number \port suffix like 8080 if the \port number of this request # is not the default HTTP \port 80 or HTTPS \port 443. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req.optional_port # => nil # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.optional_port # => 8080 def optional_port standard_port? ? nil : port @@ -341,14 +305,10 @@ module ActionDispatch # Returns a string \port suffix, including colon, like ":8080" if the \port # number of this request is not the default HTTP \port 80 or HTTPS \port 443. # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'HTTP_HOST' => 'example.com:80' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:80' # req.port_string # => "" # - # req = Request.new 'HTTP_HOST' => 'example.com:8080' + # req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080' # req.port_string # => ":8080" def port_string standard_port? ? "" : ":#{port}" @@ -356,14 +316,10 @@ module ActionDispatch # Returns the requested port, such as 8080, based on SERVER_PORT # - # class Request < Rack::Request - # include ActionDispatch::Http::URL - # end - # - # req = Request.new 'SERVER_PORT' => '80' + # req = ActionDispatch::Request.new 'SERVER_PORT' => '80' # req.server_port # => 80 # - # req = Request.new 'SERVER_PORT' => '8080' + # req = ActionDispatch::Request.new 'SERVER_PORT' => '8080' # req.server_port # => 8080 def server_port get_header("SERVER_PORT").to_i diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 0b81d0ad43..992daab3aa 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -133,12 +133,20 @@ module ActionDispatch end def redirect_to_https(request) - [ @redirect.fetch(:status, 301), + [ @redirect.fetch(:status, redirection_status(request)), { "Content-Type" => "text/html", "Location" => https_location_for(request) }, @redirect.fetch(:body, []) ] end + def redirection_status(request) + if request.get? || request.head? + 301 # Issue a permanent redirect via a GET request. + else + 307 # Issue a fresh request redirect to preserve the HTTP method. + end + end + def https_location_for(request) host = @redirect[:host] || request.host port = @redirect[:port] || request.port diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb index cabbe2d608..e5f24f1a3a 100644 --- a/actionpack/test/controller/flash_test.rb +++ b/actionpack/test/controller/flash_test.rb @@ -263,6 +263,13 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest flash[:bar] = "for great justice" head :ok end + + def set_flash_optionally + flash.now.notice = params[:flash] + if stale? etag: "abe" + render inline: "maybe flash" + end + end end def test_flash @@ -310,6 +317,28 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest end end + def test_flash_factored_into_etag + with_test_route_set do + get "/set_flash_optionally" + no_flash_etag = response.etag + + get "/set_flash_optionally", params: { flash: "hello!" } + hello_flash_etag = response.etag + + assert_not_equal no_flash_etag, hello_flash_etag + + get "/set_flash_optionally", params: { flash: "hello!" } + another_hello_flash_etag = response.etag + + assert_equal another_hello_flash_etag, hello_flash_etag + + get "/set_flash_optionally", params: { flash: "goodbye!" } + goodbye_flash_etag = response.etag + + assert_not_equal another_hello_flash_etag, goodbye_flash_etag + end + end + private # Overwrite get to send SessionSecret in env hash diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index ccddb90bb5..71b274bf1e 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -38,6 +38,16 @@ class RedirectSSLTest < SSLTest assert_equal redirect[:body].join, @response.body end + def assert_post_redirected(redirect: {}, from: "http://a/b?c=d", + to: from.sub("http", "https")) + + self.app = build_app ssl_options: { redirect: redirect } + + post from + assert_response redirect[:status] || 307 + assert_redirected_to to + end + test "exclude can avoid redirect" do excluding = { exclude: -> request { request.path =~ /healthcheck/ } } @@ -57,6 +67,10 @@ class RedirectSSLTest < SSLTest assert_redirected end + test "http POST is redirected to https with status 307" do + assert_post_redirected + end + test "redirect with non-301 status" do assert_redirected redirect: { status: 307 } end |