forever-turbolinks 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ module Turbolinks
2
+ VERSION = '3.0.0'
3
+ 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
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <script language="javascript">alert("you shouldn't see this");</script>
4
+ </body>
5
+ </html>
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>
@@ -0,0 +1,10 @@
1
+ CACHE MANIFEST
2
+
3
+ CACHE:
4
+ /offline.html
5
+
6
+ NETWORK:
7
+ *
8
+
9
+ FALLBACK:
10
+ /fallback.html /offline.html