Skip to content

Commit

Permalink
Don't support pushState when the original request method was anything…
Browse files Browse the repository at this point in the history
… other than GET

We cannot use pushState if the initial request method is a POST for two reasons:

1. Up.js replaces the initial state so it can handle the pop event when the
   user goes back to the initial URL later. If the initial request was a POST,
   Up.js will wrongly assumed that it can restore the state by reloading with GET.

2. Some browsers have a bug where the initial request method is used for all
   subsequently pushed states. That means if the user reloads the page on a later
   GET state, the browser will wrongly attempt a POST request.
   Modern Firefoxes, Chromes and IE10+ don't seem to be affected by this,
   but we saw this behavior with Safari 8 and IE9 (IE9 can't do pushState anyway).

The way that we work around this is that we don't support pushState if the
initial request method was anything other than GET (but allow the rest of the
Up.js framework to work). This way Up.js will fall back to full page loads until
the framework was booted from a GET request.
  • Loading branch information
triskweline committed Oct 12, 2015
1 parent 85822e7 commit d81d900
Show file tree
Hide file tree
Showing 6 changed files with 70 additions and 10 deletions.
38 changes: 33 additions & 5 deletions lib/assets/javascripts/up/browser.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ we can't currently get rid off.
@class up.browser
###
up.browser = (->

u = up.util

loadPage = (url, options = {}) ->
Expand Down Expand Up @@ -47,7 +47,23 @@ up.browser = (->
window.console.groupEnd ||= noop

canPushState = u.memoize ->
u.isDefined(history.pushState)
# We cannot use pushState if the initial request method is a POST for two reasons:
#
# 1. Up.js replaces the initial state so it can handle the pop event when the
# user goes back to the initial URL later. If the initial request was a POST,
# Up.js will wrongly assumed that it can restore the state by reloading with GET.
#
# 2. Some browsers have a bug where the initial request method is used for all
# subsequently pushed states. That means if the user reloads the page on a later
# GET state, the browser will wrongly attempt a POST request.
# Modern Firefoxes, Chromes and IE10+ don't seem to be affected by this,
# but we saw this behavior with Safari 8 and IE9 (IE9 can't do pushState anyway).
#
# The way that we work around this is that we don't support pushState if the
# initial request method was anything other than GET (but allow the rest of the
# Up.js framework to work). This way Up.js will fall back to full page loads until
# the framework was booted from a GET request.
u.isDefined(history.pushState) && initialRequestMethod == 'get'

canCssAnimation = u.memoize ->
'transition' of document.documentElement.style
Expand All @@ -62,7 +78,20 @@ up.browser = (->
minor = parseInt(parts[1])
compatible = major >= 2 || (major == 1 && minor >= 9)
compatible or u.error("jQuery %o found, but Up.js requires 1.9+", version)


# Returns and deletes a cookie with the given name
# Inspired by Turbolinks: https://github.com/rails/turbolinks/blob/83d4b3d2c52a681f07900c28adb28bc8da604733/lib/assets/javascripts/turbolinks.coffee#L292
popCookie = (name) ->
value = document.cookie.match(new RegExp(name+"=(\\w+)"))?[1]
if u.isPresent(value)
document.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
value

# Server-side companion libraries like upjs-rails set this cookie so we
# have a way to detect the request method of the initial page load.
# There is no Javascript API for this.
initialRequestMethod = (popCookie('_up_request_method') || 'get').toLowerCase()

isSupported = u.memoize ->
# This is the most concise way to exclude IE8 and lower
# while keeping all relevant desktop and mobile browsers.
Expand All @@ -76,6 +105,5 @@ up.browser = (->
canInputEvent: canInputEvent
isSupported: isSupported
ensureRecentJquery: ensureRecentJquery

)()

)()
3 changes: 3 additions & 0 deletions lib/assets/javascripts/up/modal.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ up.modal = (->
height = u.option(options.height, $link.attr('up-height'), config.height)
animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation)
sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'))
# Although we usually fall back to full page loads if a browser doesn't support pushState,
# in the case of modals we assume that the developer would rather see a dialog
# without an URL update.
history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($link, 'up-history'), true) else false
animateOptions = up.motion.animateOptions(options, $link)

Expand Down
5 changes: 3 additions & 2 deletions lib/upjs-rails.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require "upjs/rails/version"
require "upjs/rails/engine"
require "upjs/rails/current_location"
require "upjs/rails/request"
require "upjs/rails/request_echo_headers"
require "upjs/rails/request_method_cookie"
require "upjs/rails/request_ext"
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
module Upjs
module Rails
module CurrentLocation
module RequestEchoHeaders

def self.included(base)
base.before_filter :set_header_for_current_location
base.before_filter :set_up_request_echo_headers
end

private

def set_header_for_current_location
def set_up_request_echo_headers
headers['X-Up-Location'] = request.original_url
headers['X-Up-Method'] = request.method
end
Expand Down
File renamed without changes.
28 changes: 28 additions & 0 deletions lib/upjs/rails/request_method_cookie.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# See
# https://github.com/rails/turbolinks/search?q=request_method&ref=cmdform
# https://github.com/rails/turbolinks/blob/83d4b3d2c52a681f07900c28adb28bc8da604733/README.md#initialization
module Upjs
module Rails
module RequestMethod

COOKIE_NAME = '_up_request_method'

def self.included(base)
base.before_filter :set_up_request_method_cookie
end

private

def set_up_request_method_cookie
if request.get?
cookies.delete(COOKIE_NAME)
else
cookies[COOKIE_NAME] = request.request_method
end
end

ActionController::Base.send(:include, self)

end
end
end

0 comments on commit d81d900

Please sign in to comment.