forever-turbolinks 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +537 -0
- data/lib/assets/javascripts/turbolinks.coffee +733 -0
- data/lib/turbolinks.rb +64 -0
- data/lib/turbolinks/cookies.rb +15 -0
- data/lib/turbolinks/redirection.rb +77 -0
- data/lib/turbolinks/version.rb +3 -0
- data/lib/turbolinks/x_domain_blocker.rb +22 -0
- data/lib/turbolinks/xhr_headers.rb +44 -0
- data/lib/turbolinks/xhr_redirect.rb +30 -0
- data/lib/turbolinks/xhr_url_for.rb +23 -0
- data/test/attachment.html +5 -0
- data/test/config.ru +65 -0
- data/test/dummy.gif +0 -0
- data/test/form.html +17 -0
- data/test/index.html +53 -0
- data/test/manifest.appcache +10 -0
- data/test/offline.html +19 -0
- data/test/other.html +26 -0
- data/test/partial1.html +34 -0
- data/test/partial2.html +26 -0
- data/test/partial3.html +28 -0
- data/test/redirect1.html +16 -0
- data/test/redirect2.html +13 -0
- data/test/reload.html +18 -0
- data/test/withoutextension +26 -0
- metadata +113 -0
data/lib/turbolinks.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'turbolinks/version'
|
2
|
+
require 'turbolinks/xhr_headers'
|
3
|
+
require 'turbolinks/xhr_redirect'
|
4
|
+
require 'turbolinks/xhr_url_for'
|
5
|
+
require 'turbolinks/cookies'
|
6
|
+
require 'turbolinks/x_domain_blocker'
|
7
|
+
require 'turbolinks/redirection'
|
8
|
+
|
9
|
+
module Turbolinks
|
10
|
+
module Controller
|
11
|
+
include XHRHeaders, Cookies, XDomainBlocker, Redirection
|
12
|
+
|
13
|
+
def self.included(base)
|
14
|
+
if base.respond_to?(:before_action)
|
15
|
+
base.before_action :set_xhr_redirected_to, :set_request_method_cookie
|
16
|
+
base.after_action :abort_xdomain_redirect
|
17
|
+
else
|
18
|
+
base.before_filter :set_xhr_redirected_to, :set_request_method_cookie
|
19
|
+
base.after_filter :abort_xdomain_redirect
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Engine < ::Rails::Engine
|
25
|
+
config.turbolinks = ActiveSupport::OrderedOptions.new
|
26
|
+
config.turbolinks.auto_include = true
|
27
|
+
|
28
|
+
initializer :turbolinks do |app|
|
29
|
+
ActiveSupport.on_load(:action_controller) do
|
30
|
+
next if self != ActionController::Base
|
31
|
+
|
32
|
+
if app.config.turbolinks.auto_include
|
33
|
+
include Controller
|
34
|
+
end
|
35
|
+
|
36
|
+
ActionDispatch::Request.class_eval do
|
37
|
+
def referer
|
38
|
+
self.headers['X-XHR-Referer'] || super
|
39
|
+
end
|
40
|
+
alias referrer referer
|
41
|
+
end
|
42
|
+
|
43
|
+
require 'action_dispatch/routing/redirection'
|
44
|
+
ActionDispatch::Routing::Redirect.class_eval do
|
45
|
+
if defined?(prepend)
|
46
|
+
prepend XHRRedirect
|
47
|
+
else
|
48
|
+
include LegacyXHRRedirect
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
ActiveSupport.on_load(:action_view) do
|
54
|
+
(ActionView::RoutingUrlFor rescue ActionView::Helpers::UrlHelper).module_eval do
|
55
|
+
if defined?(prepend) && Rails.version >= '4'
|
56
|
+
prepend XHRUrlFor
|
57
|
+
else
|
58
|
+
include LegacyXHRUrlFor
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
# For non-GET requests, sets a request_method cookie containing
|
3
|
+
# the request method of the current request. The Turbolinks script
|
4
|
+
# will not initialize if this cookie is set.
|
5
|
+
module Cookies
|
6
|
+
private
|
7
|
+
def set_request_method_cookie
|
8
|
+
if request.get?
|
9
|
+
cookies.delete(:request_method)
|
10
|
+
else
|
11
|
+
cookies[:request_method] = request.request_method
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
# Provides a means of using Turbolinks to perform renders and redirects.
|
3
|
+
# The server will respond with a JavaScript call to Turbolinks.visit/replace().
|
4
|
+
module Redirection
|
5
|
+
MUTATION_MODES = [:change, :append, :prepend].freeze
|
6
|
+
|
7
|
+
def redirect_to(url = {}, response_status = {})
|
8
|
+
turbolinks, options = _extract_turbolinks_options!(response_status)
|
9
|
+
turbolinks = (request.xhr? && (options.size > 0 || !request.get?)) if turbolinks.nil?
|
10
|
+
|
11
|
+
if turbolinks
|
12
|
+
response.content_type = Mime[:js]
|
13
|
+
end
|
14
|
+
|
15
|
+
return_value = super(url, response_status)
|
16
|
+
|
17
|
+
if turbolinks
|
18
|
+
self.status = 200
|
19
|
+
self.response_body = "Turbolinks.visit('#{location}'#{_turbolinks_js_options(options)});"
|
20
|
+
end
|
21
|
+
|
22
|
+
return_value
|
23
|
+
end
|
24
|
+
|
25
|
+
def render(*args, &block)
|
26
|
+
render_options = args.extract_options!
|
27
|
+
turbolinks, options = _extract_turbolinks_options!(render_options)
|
28
|
+
turbolinks = (request.xhr? && options.size > 0) if turbolinks.nil?
|
29
|
+
|
30
|
+
if turbolinks
|
31
|
+
response.content_type = Mime[:js]
|
32
|
+
|
33
|
+
render_options = _normalize_render(*args, render_options, &block)
|
34
|
+
body = render_to_body(render_options)
|
35
|
+
|
36
|
+
self.status = 200
|
37
|
+
self.response_body = "Turbolinks.replace('#{view_context.j(body)}'#{_turbolinks_js_options(options)});"
|
38
|
+
else
|
39
|
+
super(*args, render_options, &block)
|
40
|
+
end
|
41
|
+
|
42
|
+
self.response_body
|
43
|
+
end
|
44
|
+
|
45
|
+
def redirect_via_turbolinks_to(url = {}, response_status = {})
|
46
|
+
ActiveSupport::Deprecation.warn("`redirect_via_turbolinks_to` is deprecated and will be removed in Turbolinks 3.1. Use redirect_to(url, turbolinks: true) instead.")
|
47
|
+
redirect_to(url, response_status.merge!(turbolinks: true))
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def _extract_turbolinks_options!(options)
|
52
|
+
turbolinks = options.delete(:turbolinks)
|
53
|
+
options = options.extract!(:keep, :change, :append, :prepend, :flush).delete_if { |_, value| value.nil? }
|
54
|
+
|
55
|
+
raise ArgumentError, "cannot combine :keep and :flush options" if options[:keep] && options[:flush]
|
56
|
+
|
57
|
+
MUTATION_MODES.each do |mutation_mode_option|
|
58
|
+
raise ArgumentError, "cannot combine :keep and :#{mutation_mode_option} options" if options[:keep] && options[mutation_mode_option]
|
59
|
+
raise ArgumentError, "cannot combine :flush and :#{mutation_mode_option} options" if options[:flush] && options[mutation_mode_option]
|
60
|
+
end if options[:keep] || options[:flush]
|
61
|
+
|
62
|
+
[turbolinks, options]
|
63
|
+
end
|
64
|
+
|
65
|
+
def _turbolinks_js_options(options)
|
66
|
+
js_options = {}
|
67
|
+
|
68
|
+
js_options[:change] = Array(options[:change]) if options[:change]
|
69
|
+
js_options[:append] = Array(options[:append]) if options[:append]
|
70
|
+
js_options[:prepend] = Array(options[:prepend]) if options[:prepend]
|
71
|
+
js_options[:keep] = Array(options[:keep]) if options[:keep]
|
72
|
+
js_options[:flush] = true if options[:flush]
|
73
|
+
|
74
|
+
", #{js_options.to_json}" if js_options.present?
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
# Changes the response status to 403 Forbidden if all of these conditions are true:
|
3
|
+
# - The current request originated from Turbolinks
|
4
|
+
# - The request is being redirected to a different domain
|
5
|
+
module XDomainBlocker
|
6
|
+
private
|
7
|
+
def same_origin?(a, b)
|
8
|
+
a = URI.parse URI.escape(a)
|
9
|
+
b = URI.parse URI.escape(b)
|
10
|
+
[a.scheme, a.host, a.port] == [b.scheme, b.host, b.port]
|
11
|
+
end
|
12
|
+
|
13
|
+
def abort_xdomain_redirect
|
14
|
+
to_uri = response.headers['Location']
|
15
|
+
current = request.headers['X-XHR-Referer']
|
16
|
+
unless to_uri.blank? || current.blank? || same_origin?(current, to_uri)
|
17
|
+
self.status = 403
|
18
|
+
end
|
19
|
+
rescue URI::InvalidURIError
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
# Intercepts calls to _compute_redirect_to_location (used by redirect_to) for two purposes.
|
3
|
+
#
|
4
|
+
# 1. Corrects the behavior of redirect_to with the :back option by using the X-XHR-Referer
|
5
|
+
# request header instead of the standard Referer request header.
|
6
|
+
#
|
7
|
+
# 2. Stores the return value (the redirect target url) to persist through to the redirect
|
8
|
+
# request, where it will be used to set the X-XHR-Redirected-To response header. The
|
9
|
+
# Turbolinks script will detect the header and use replaceState to reflect the redirected
|
10
|
+
# url.
|
11
|
+
module XHRHeaders
|
12
|
+
def _compute_redirect_to_location(*args)
|
13
|
+
options, request = _normalize_redirect_params(args)
|
14
|
+
|
15
|
+
store_for_turbolinks begin
|
16
|
+
if options == :back && request.headers["X-XHR-Referer"]
|
17
|
+
super(*[(request if args.length == 2), request.headers["X-XHR-Referer"]].compact)
|
18
|
+
else
|
19
|
+
super(*args)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def store_for_turbolinks(url)
|
26
|
+
session[:_turbolinks_redirect_to] = url if session && request.headers["X-XHR-Referer"]
|
27
|
+
url
|
28
|
+
end
|
29
|
+
|
30
|
+
def set_xhr_redirected_to
|
31
|
+
if session && session[:_turbolinks_redirect_to]
|
32
|
+
response.headers['X-XHR-Redirected-To'] = session.delete :_turbolinks_redirect_to
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Ensure backwards compatibility
|
37
|
+
# Rails < 4.2: _compute_redirect_to_location(options)
|
38
|
+
# Rails >= 4.2: _compute_redirect_to_location(request, options)
|
39
|
+
def _normalize_redirect_params(args)
|
40
|
+
options, req = args.reverse
|
41
|
+
[options, req || request]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
module XHRRedirect
|
3
|
+
def call(env)
|
4
|
+
status, headers, body = super(env)
|
5
|
+
|
6
|
+
if env['rack.session'] && env['HTTP_X_XHR_REFERER']
|
7
|
+
env['rack.session'][:_turbolinks_redirect_to] = headers['Location']
|
8
|
+
end
|
9
|
+
|
10
|
+
[status, headers, body]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# TODO: Remove me when support for Ruby < 2 && Rails < 4 is dropped
|
15
|
+
module LegacyXHRRedirect
|
16
|
+
def self.included(base)
|
17
|
+
base.alias_method_chain :call, :turbolinks
|
18
|
+
end
|
19
|
+
|
20
|
+
def call_with_turbolinks(env)
|
21
|
+
status, headers, body = call_without_turbolinks(env)
|
22
|
+
|
23
|
+
if env['rack.session'] && env['HTTP_X_XHR_REFERER']
|
24
|
+
env['rack.session'][:_turbolinks_redirect_to] = headers['Location']
|
25
|
+
end
|
26
|
+
|
27
|
+
[status, headers, body]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Turbolinks
|
2
|
+
# Corrects the behavior of url_for (and link_to, which uses url_for) with the :back
|
3
|
+
# option by using the X-XHR-Referer request header instead of the standard Referer
|
4
|
+
# request header.
|
5
|
+
module XHRUrlFor
|
6
|
+
def url_for(options = {})
|
7
|
+
options = (controller.request.headers["X-XHR-Referer"] || options) if options == :back
|
8
|
+
super
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# TODO: Remove me when support for Ruby < 2 && Rails < 4 is dropped
|
13
|
+
module LegacyXHRUrlFor
|
14
|
+
def self.included(base)
|
15
|
+
base.alias_method_chain :url_for, :xhr_referer
|
16
|
+
end
|
17
|
+
|
18
|
+
def url_for_with_xhr_referer(options = {})
|
19
|
+
options = (controller.request.headers["X-XHR-Referer"] || options) if options == :back
|
20
|
+
url_for_without_xhr_referer options
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/test/config.ru
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'sprockets'
|
2
|
+
require 'coffee-script'
|
3
|
+
|
4
|
+
Root = File.expand_path("../..", __FILE__)
|
5
|
+
|
6
|
+
Assets = Sprockets::Environment.new do |env|
|
7
|
+
env.append_path File.join(Root, "lib", "assets", "javascripts")
|
8
|
+
env.append_path File.join(Root, "node_modules", "mocha")
|
9
|
+
env.append_path File.join(Root, "node_modules", "chai")
|
10
|
+
env.append_path File.join(Root, "node_modules", "jquery", "dist")
|
11
|
+
env.append_path File.join(Root, "test", "javascript")
|
12
|
+
end
|
13
|
+
|
14
|
+
class SlowResponse
|
15
|
+
CHUNKS = ['<html><body>', '.'*50, '.'*20, '<a href="/index.html">Home</a></body></html>']
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
[200, headers, self]
|
19
|
+
end
|
20
|
+
|
21
|
+
def each
|
22
|
+
CHUNKS.each do |part|
|
23
|
+
sleep rand(0.3..0.8)
|
24
|
+
yield part
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def length
|
29
|
+
CHUNKS.join.length
|
30
|
+
end
|
31
|
+
|
32
|
+
def headers
|
33
|
+
{ "Content-Length" => length.to_s, "Content-Type" => "text/html", "Cache-Control" => "no-cache, no-store, must-revalidate" }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
map "/js" do
|
38
|
+
run Assets
|
39
|
+
end
|
40
|
+
|
41
|
+
map "/500" do
|
42
|
+
# throw Internal Server Error (500)
|
43
|
+
end
|
44
|
+
|
45
|
+
map "/withoutextension" do
|
46
|
+
run Rack::File.new(File.join(Root, "test", "withoutextension"), "Content-Type" => "text/html")
|
47
|
+
end
|
48
|
+
|
49
|
+
map "/slow-response" do
|
50
|
+
run SlowResponse.new
|
51
|
+
end
|
52
|
+
|
53
|
+
map "/bounce" do
|
54
|
+
run Proc.new{ [200, { "X-XHR-Redirected-To" => "redirect1.html", "Content-Type" => "text/html" }, File.open( File.join( Root, "test", "redirect1.html" ) ) ] }
|
55
|
+
end
|
56
|
+
|
57
|
+
map "/attachment.txt" do
|
58
|
+
run Rack::File.new(File.join(Root, "test", "attachment.html"), "Content-Type" => "text/plain")
|
59
|
+
end
|
60
|
+
|
61
|
+
map "/attachment.html" do
|
62
|
+
run Rack::File.new(File.join(Root, "test", "attachment.html"), "Content-Type" => "text/html", "Content-Disposition" => "attachment; filename=attachment.html")
|
63
|
+
end
|
64
|
+
|
65
|
+
run Rack::Directory.new(File.join(Root, "test"))
|
data/test/dummy.gif
ADDED
Binary file
|
data/test/form.html
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<title>Home</title>
|
6
|
+
<script type="text/javascript" src="/js/turbolinks.js"></script>
|
7
|
+
</head>
|
8
|
+
<body class="page-other">
|
9
|
+
<form action="form.html" method="get">
|
10
|
+
<input type="text" name="value" value="42" />
|
11
|
+
<button type="submit">Submit</button>
|
12
|
+
</form>
|
13
|
+
<ul>
|
14
|
+
<li><a href="/index.html">Home</a></li>
|
15
|
+
</ul>
|
16
|
+
</body>
|
17
|
+
</html>
|
data/test/index.html
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<title>Home</title>
|
6
|
+
<script type="text/javascript" src="/js/turbolinks.js"></script>
|
7
|
+
<script type="text/javascript">
|
8
|
+
document.addEventListener("page:change", function() {
|
9
|
+
console.log("page changed");
|
10
|
+
});
|
11
|
+
|
12
|
+
document.addEventListener("page:update", function() {
|
13
|
+
console.log("page updated");
|
14
|
+
});
|
15
|
+
|
16
|
+
document.addEventListener("page:restore", function() {
|
17
|
+
console.log("page restored");
|
18
|
+
});
|
19
|
+
</script>
|
20
|
+
</head>
|
21
|
+
<body class="page-index">
|
22
|
+
<ul style="margin-top:20px;">
|
23
|
+
<li><a href="/other.html">Other page</a></li>
|
24
|
+
<li><a href="/form.html">Form</a></li>
|
25
|
+
<li><a href="/slow-response">Slow loading page for progress bar</a></li>
|
26
|
+
<li><a href="/other.html"><span>Wrapped link</span></a></li>
|
27
|
+
<li><a href="/withoutextension">Without extension</a></li>
|
28
|
+
<li><a href="/withoutextension?sort=user.name">Without extension with query params</a></li>
|
29
|
+
<li><a href="http://www.google.com/">Cross origin</a></li>
|
30
|
+
<li><a href="/other.html" onclick="if(!confirm('follow link?')) { return false}">Confirm Fire Order</a></li>
|
31
|
+
<li><a href="/reload.html"><span>New assets track </span></a></li>
|
32
|
+
<li><a href="/partial1.html" data-no-turbolink>Partial replacement</a></li>
|
33
|
+
<li><a href="/dummy.gif?12345">Query Param Image Link</a></li>
|
34
|
+
<li><a href="/bounce">Redirect</a></li>
|
35
|
+
<li><a href="#">Hash link</a></li>
|
36
|
+
<li><a href="/reload.html#foo">New assets track with hash link</a></li>
|
37
|
+
<li><a href="/attachment.txt">A text response should load normally</a></li>
|
38
|
+
<li><a href="/attachment.html">An html response with Content-Disposition: attachment should load normally</a></li>
|
39
|
+
<li><h5>If you stop the server or go into airplane/offline mode</h5></li>
|
40
|
+
<li><a href="/doesnotexist.html">A page with client error (4xx, rfc2616 sec. 10.4) should error out</a></li>
|
41
|
+
<li><a href="/500">Also server errors (5xx, rfc2616 sec. 10.5) should error out</a></li>
|
42
|
+
<li><a href="/fallback.html">A page that has a fallback in appcache should fallback</a></li>
|
43
|
+
</ul>
|
44
|
+
|
45
|
+
<div style="background:#ccc;height:5000px;width:200px;">
|
46
|
+
</div>
|
47
|
+
<iframe height='1' scrolling='no' src='/offline.html' style='display: none;' width='1'></iframe>
|
48
|
+
|
49
|
+
<script type="text/javascript" data-turbolinks-eval=false>
|
50
|
+
console.log("turbolinks-eval-false script fired. This should only happen on the initial page load.");
|
51
|
+
</script>
|
52
|
+
</body>
|
53
|
+
</html>
|