turbo_boost-commands 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of turbo_boost-commands might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/app/assets/builds/@turbo-boost/commands.js +1 -1
- data/app/assets/builds/@turbo-boost/commands.js.map +4 -4
- data/app/assets/builds/@turbo-boost/commands.metafile.json +1 -1
- data/app/controllers/concerns/turbo_boost/commands/controller.rb +0 -6
- data/app/javascript/drivers/form.js +2 -2
- data/app/javascript/drivers/frame.js +2 -7
- data/app/javascript/drivers/index.js +1 -1
- data/app/javascript/drivers/method.js +29 -8
- data/app/javascript/drivers/window.js +2 -54
- data/app/javascript/headers.js +43 -0
- data/app/javascript/index.js +5 -9
- data/app/javascript/invoker.js +39 -0
- data/app/javascript/logger.js +29 -8
- data/app/javascript/renderer.js +14 -12
- data/app/javascript/turbo.js +18 -27
- data/app/javascript/urls.js +8 -6
- data/app/javascript/version.js +1 -1
- data/lib/turbo_boost/commands/command.rb +6 -1
- data/lib/turbo_boost/commands/controller_pack.rb +6 -1
- data/lib/turbo_boost/commands/engine.rb +5 -4
- data/lib/turbo_boost/commands/middleware.rb +85 -0
- data/lib/turbo_boost/commands/runner.rb +88 -53
- data/lib/turbo_boost/commands/state.rb +11 -16
- data/lib/turbo_boost/commands/version.rb +1 -1
- metadata +35 -4
@@ -0,0 +1,39 @@
|
|
1
|
+
import headers from './headers'
|
2
|
+
import lifecycle from './lifecycle'
|
3
|
+
import state from './state'
|
4
|
+
import urls from './urls'
|
5
|
+
import { dispatch } from './events'
|
6
|
+
import { render } from './renderer'
|
7
|
+
|
8
|
+
const parseError = error => {
|
9
|
+
const errorMessage = `Unexpected error performing a TurboBoost Command! ${error.message}`
|
10
|
+
dispatch(lifecycle.events.clientError, document, { detail: { error: errorMessage } }, true)
|
11
|
+
}
|
12
|
+
|
13
|
+
const parseAndRenderResponse = response => {
|
14
|
+
const { strategy } = headers.tokenize(response.headers.get(headers.RESPONSE_HEADER))
|
15
|
+
|
16
|
+
// FAIL: Status outside the range of 200-399
|
17
|
+
if (response.status < 200 || response.status > 399) {
|
18
|
+
const error = `Server returned a ${response.status} status code! TurboBoost Commands require 2XX-3XX status codes.`
|
19
|
+
dispatch(lifecycle.events.serverError, document, { detail: { error, response } }, true)
|
20
|
+
}
|
21
|
+
|
22
|
+
response.text().then(content => render(strategy, content))
|
23
|
+
}
|
24
|
+
|
25
|
+
const invoke = (payload = {}) => {
|
26
|
+
try {
|
27
|
+
fetch(urls.commandInvocationURL.href, {
|
28
|
+
method: 'POST',
|
29
|
+
headers: headers.prepare({}),
|
30
|
+
body: JSON.stringify(payload)
|
31
|
+
})
|
32
|
+
.then(parseAndRenderResponse)
|
33
|
+
.catch(parseError)
|
34
|
+
} catch (error) {
|
35
|
+
parseError(error)
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
export { invoke }
|
data/app/javascript/logger.js
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import { allEvents as events } from './events'
|
2
2
|
|
3
3
|
let currentLevel = 'unknown'
|
4
|
+
let initialized = false
|
5
|
+
let history = []
|
4
6
|
|
5
7
|
const logLevels = {
|
6
8
|
debug: Object.values(events),
|
@@ -10,14 +12,33 @@ const logLevels = {
|
|
10
12
|
unknown: []
|
11
13
|
}
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
}
|
15
|
+
const shouldLogEvent = event => {
|
16
|
+
if (!logLevels[currentLevel].includes(event.type)) return false
|
17
|
+
if (typeof console[currentLevel] !== 'function') return false
|
18
|
+
|
19
|
+
const { detail } = event
|
20
|
+
if (!detail.id) return true
|
21
|
+
|
22
|
+
const key = `${event.type}-${detail.id}`
|
23
|
+
if (history.includes(key)) return false
|
24
|
+
|
25
|
+
if (history.length > 16) history.shift()
|
26
|
+
history.push(key)
|
27
|
+
|
28
|
+
return true
|
29
|
+
}
|
30
|
+
|
31
|
+
const logEvent = event => {
|
32
|
+
if (shouldLogEvent(event)) {
|
33
|
+
const { target, type, detail } = event
|
34
|
+
console[currentLevel](type, detail.id || '', { target, detail })
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
if (!initialized) {
|
39
|
+
initialized = true
|
40
|
+
Object.values(events).forEach(name => addEventListener(name, event => logEvent(event)))
|
41
|
+
}
|
21
42
|
|
22
43
|
export default {
|
23
44
|
get level() {
|
data/app/javascript/renderer.js
CHANGED
@@ -1,16 +1,18 @@
|
|
1
|
-
|
2
|
-
const head = '<html'
|
3
|
-
const tail = '</html'
|
4
|
-
const headIndex = content.indexOf(head)
|
5
|
-
const tailIndex = content.lastIndexOf(tail)
|
6
|
-
if (headIndex >= 0 && tailIndex >= 0) {
|
7
|
-
const html = content.slice(content.indexOf('>', headIndex) + 1, tailIndex)
|
8
|
-
document.documentElement.innerHTML = html
|
9
|
-
}
|
10
|
-
}
|
1
|
+
import uuids from './uuids'
|
11
2
|
|
12
|
-
|
3
|
+
const append = content => {
|
13
4
|
document.body.insertAdjacentHTML('beforeend', content)
|
14
5
|
}
|
15
6
|
|
16
|
-
|
7
|
+
const replace = content => {
|
8
|
+
const parser = new DOMParser()
|
9
|
+
const doc = parser.parseFromString(content, 'text/html')
|
10
|
+
TurboBoost.Streams.morph.method(document.documentElement, doc.documentElement)
|
11
|
+
}
|
12
|
+
|
13
|
+
export const render = (strategy, content) => {
|
14
|
+
if (strategy.match(/^Append$/i)) return append(content)
|
15
|
+
if (strategy.match(/^Replace$/i)) return replace(content)
|
16
|
+
}
|
17
|
+
|
18
|
+
export default { render }
|
data/app/javascript/turbo.js
CHANGED
@@ -1,46 +1,37 @@
|
|
1
|
-
import
|
2
|
-
import renderer from './renderer'
|
3
|
-
import { dispatch } from './events'
|
1
|
+
import headers from './headers'
|
4
2
|
import lifecycle from './lifecycle'
|
3
|
+
import { dispatch } from './events'
|
4
|
+
import { render } from './renderer'
|
5
5
|
|
6
6
|
const frameSources = {}
|
7
7
|
|
8
|
-
// fires before making a turbo HTTP request
|
9
|
-
addEventListener('turbo:before-fetch-request', event => {
|
10
|
-
const frame = event.target.closest('turbo-frame')
|
11
|
-
const { fetchOptions } = event.detail
|
12
|
-
|
13
|
-
// command invoked and busy
|
14
|
-
if (self.TurboBoost?.Commands?.busy) {
|
15
|
-
let acceptHeaders = ['text/vnd.turbo-boost.html', fetchOptions.headers['Accept']]
|
16
|
-
acceptHeaders = acceptHeaders.filter(entry => entry && entry.trim().length > 0).join(', ')
|
17
|
-
fetchOptions.headers['Accept'] = acceptHeaders
|
18
|
-
}
|
19
|
-
})
|
20
|
-
|
21
8
|
// fires after receiving a turbo HTTP response
|
22
9
|
addEventListener('turbo:before-fetch-response', event => {
|
23
10
|
const frame = event.target.closest('turbo-frame')
|
11
|
+
if (frame?.id && frame?.src) frameSources[frame.id] = frame.src
|
12
|
+
|
24
13
|
const { fetchResponse: response } = event.detail
|
14
|
+
const header = response.header(headers.RESPONSE_HEADER)
|
25
15
|
|
26
|
-
if (
|
16
|
+
if (!header) return
|
27
17
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
}
|
18
|
+
// We'll take it from here Hotwire...
|
19
|
+
event.preventDefault()
|
20
|
+
const { statusCode } = response
|
21
|
+
const { strategy } = headers.tokenize(header)
|
33
22
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
}
|
23
|
+
// FAIL: Status outside the range of 200-399
|
24
|
+
if (statusCode < 200 || statusCode > 399) {
|
25
|
+
const error = `Server returned a ${status} status code! TurboBoost Commands require 2XX-3XX status codes.`
|
26
|
+
dispatch(lifecycle.events.clientError, document, { detail: { error, response } }, true)
|
38
27
|
}
|
28
|
+
|
29
|
+
response.responseHTML.then(content => render(strategy, content))
|
39
30
|
})
|
40
31
|
|
41
32
|
// fires when a frame element is navigated and finishes loading
|
42
33
|
addEventListener('turbo:frame-load', event => {
|
43
34
|
const frame = event.target.closest('turbo-frame')
|
44
|
-
frame.dataset.
|
35
|
+
frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src
|
45
36
|
delete frameSources[frame.id]
|
46
37
|
})
|
data/app/javascript/urls.js
CHANGED
@@ -1,9 +1,11 @@
|
|
1
|
-
|
1
|
+
const buildURL = path => {
|
2
2
|
const a = document.createElement('a')
|
3
|
-
a.href =
|
4
|
-
|
5
|
-
url.searchParams.set('tbc', JSON.stringify(payload))
|
6
|
-
return url
|
3
|
+
a.href = path
|
4
|
+
return new URL(a)
|
7
5
|
}
|
8
6
|
|
9
|
-
export default {
|
7
|
+
export default {
|
8
|
+
get commandInvocationURL() {
|
9
|
+
return buildURL('/turbo-boost-command-invocation')
|
10
|
+
}
|
11
|
+
}
|
data/app/javascript/version.js
CHANGED
@@ -1 +1 @@
|
|
1
|
-
export default '0.1
|
1
|
+
export default '0.2.1'
|
@@ -90,7 +90,11 @@ class TurboBoost::Commands::Command
|
|
90
90
|
@turbo_streams = Set.new
|
91
91
|
end
|
92
92
|
|
93
|
-
# Abstract
|
93
|
+
# Abstract method to resolve state (default noop), override in subclassed commands
|
94
|
+
def resolve_state(client_state)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Abstract `perform` method, override in subclassed commands
|
94
98
|
def perform
|
95
99
|
raise NotImplementedError, "#{self.class.name} must implement the `perform` method!"
|
96
100
|
end
|
@@ -113,6 +117,7 @@ class TurboBoost::Commands::Command
|
|
113
117
|
end
|
114
118
|
|
115
119
|
# Same method signature as ActionView::Rendering#render (i.e. controller.view_context.render)
|
120
|
+
# Great for rendering partials with short-hand syntax sugar → `render "/path/to/partial"`
|
116
121
|
def render(options = {}, locals = {}, &block)
|
117
122
|
return controller.view_context.render(options, locals, &block) unless options.is_a?(Hash)
|
118
123
|
|
@@ -8,6 +8,7 @@ class TurboBoost::Commands::ControllerPack
|
|
8
8
|
attr_reader :runner, :command
|
9
9
|
|
10
10
|
delegate(
|
11
|
+
:command_state,
|
11
12
|
:command_aborted?,
|
12
13
|
:command_errored?,
|
13
14
|
:command_performed?,
|
@@ -15,7 +16,6 @@ class TurboBoost::Commands::ControllerPack
|
|
15
16
|
:command_requested?,
|
16
17
|
:command_succeeded?,
|
17
18
|
:controller,
|
18
|
-
:state,
|
19
19
|
to: :runner
|
20
20
|
)
|
21
21
|
|
@@ -23,4 +23,9 @@ class TurboBoost::Commands::ControllerPack
|
|
23
23
|
@runner = TurboBoost::Commands::Runner.new(controller)
|
24
24
|
@command = runner.command_instance
|
25
25
|
end
|
26
|
+
|
27
|
+
def state
|
28
|
+
ActiveSupport::Deprecation.warn "The `state` method has been deprecated. Please update to `command_state`."
|
29
|
+
command_state
|
30
|
+
end
|
26
31
|
end
|
@@ -7,6 +7,7 @@ require_relative "version"
|
|
7
7
|
require_relative "http_status_codes"
|
8
8
|
require_relative "errors"
|
9
9
|
require_relative "patches"
|
10
|
+
require_relative "middleware"
|
10
11
|
require_relative "command"
|
11
12
|
require_relative "controller_pack"
|
12
13
|
require_relative "../../../app/controllers/concerns/turbo_boost/commands/controller"
|
@@ -19,16 +20,16 @@ module TurboBoost::Commands
|
|
19
20
|
|
20
21
|
class Engine < ::Rails::Engine
|
21
22
|
config.turbo_boost_commands = ActiveSupport::OrderedOptions.new
|
22
|
-
config.turbo_boost_commands[:
|
23
|
+
config.turbo_boost_commands[:protect_from_forgery] = true
|
24
|
+
config.turbo_boost_commands[:precompile_assets] = true
|
23
25
|
|
24
26
|
# must opt-in to state overrides
|
25
27
|
config.turbo_boost_commands[:apply_client_state_overrides] = false
|
26
28
|
config.turbo_boost_commands[:apply_server_state_overrides] = false
|
27
29
|
|
28
|
-
|
29
|
-
|
30
|
-
initializer "turbo_boost_commands.configuration" do
|
30
|
+
initializer "turbo_boost_commands.configuration" do |app|
|
31
31
|
Mime::Type.register "text/vnd.turbo-boost.html", :turbo_boost
|
32
|
+
app.middleware.insert 0, TurboBoost::Commands::Middleware
|
32
33
|
|
33
34
|
ActiveSupport.on_load :action_controller_base do
|
34
35
|
# `self` is ActionController::Base
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TurboBoost::Commands::Middleware
|
4
|
+
PATH = "/turbo-boost-command-invocation"
|
5
|
+
|
6
|
+
def initialize(app)
|
7
|
+
@app = app
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
modify! request if modify?(request)
|
13
|
+
@app.call env
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
# Returns the MIME type for TurboBoost Command invocations.
|
19
|
+
def mime_type
|
20
|
+
Mime::Type.lookup_by_extension(:turbo_boost)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Indicates whether or not the request is a TurboBoost Command invocation that requires modifications
|
24
|
+
# before we hand things over to Rails.
|
25
|
+
#
|
26
|
+
# @param request [Rack::Request] the request to check
|
27
|
+
# @return [Boolean] true if the request is a TurboBoost Command invocation, false otherwise
|
28
|
+
def modify?(request)
|
29
|
+
return false unless request.post?
|
30
|
+
return false unless request.path.start_with?(PATH)
|
31
|
+
return false unless mime_type && request.env["HTTP_ACCEPT"]&.include?(mime_type)
|
32
|
+
true
|
33
|
+
rescue => error
|
34
|
+
puts "#{self.class.name} failed to determine if the request should be modified! #{error.message}"
|
35
|
+
false
|
36
|
+
end
|
37
|
+
|
38
|
+
# Modifies the given POST request so Rails sees it as GET.
|
39
|
+
#
|
40
|
+
# The posted JSON body content holds the TurboBoost Command meta data.
|
41
|
+
# The parsed JSON body is stored in the environment under the `turbo_boost_command` key.
|
42
|
+
#
|
43
|
+
# @example POST payload for: /turbo-boost-command-invocation
|
44
|
+
# {
|
45
|
+
# "id" => "turbo-command-f824ded1-a86e-4a36-9442-ea2165a64569", # unique command invocation id
|
46
|
+
# "name" => "IncrementCountCommand", # the command being invoked
|
47
|
+
# "elementId" => nil, # the triggering element's dom id
|
48
|
+
# "elementAttributes" => {"tag"=>"BUTTON", "checked"=>false, "disabled"=>false, "value"=>nil}, # the triggering element's attributes
|
49
|
+
# "startedAt" => 1708213193567, # the time the command was invoked
|
50
|
+
# "changedState" => {}, # the delta of optimistic state changes made on the client
|
51
|
+
# "clientState" => { # the state as it was on the client
|
52
|
+
# "command_token" => "IlU0dVVhNElFdkVCZVUi--a878d33d85ed9b9611c155ed1d7bb8785fb..."} # the command token used for forgery protection
|
53
|
+
# },
|
54
|
+
# "signedState" => "eyJfcmFpbHMiOnsiZGF0YSI6ImdpZDovL2R1VuaXZlcnNhbElEOjpFeH...", # the state as it was on the server at the time of the last command invocation
|
55
|
+
# "driver" => "frame", # the driver used to invoke the command
|
56
|
+
# "frameId" => "basic_command-turbo-frame", # the turbo-frame id (if applicable)
|
57
|
+
# "src" => "/basic_command.turbo_stream" # the URL to present to Rails (turbo-frame src, window location, etc.)
|
58
|
+
# }
|
59
|
+
#
|
60
|
+
# @param request [Rack::Request] the request to modify
|
61
|
+
def modify!(request)
|
62
|
+
params = JSON.parse(request.body.string)
|
63
|
+
uri = URI.parse(params["src"])
|
64
|
+
|
65
|
+
request.env.tap do |env|
|
66
|
+
# Store the command params in the environment
|
67
|
+
env["turbo_boost_command"] = params
|
68
|
+
|
69
|
+
# Change the method from POST to GET
|
70
|
+
env["REQUEST_METHOD"] = "GET"
|
71
|
+
|
72
|
+
# Update the URI, PATH_INFO, and QUERY_STRING
|
73
|
+
env["REQUEST_URI"] = uri.to_s if env.key?("REQUEST_URI")
|
74
|
+
env["PATH_INFO"] = uri.path
|
75
|
+
env["QUERY_STRING"] = uri.query.to_s
|
76
|
+
|
77
|
+
# Clear the body and related headers so the appears and behaves like a GET
|
78
|
+
env["rack.input"] = StringIO.new
|
79
|
+
env["CONTENT_LENGTH"] = "0"
|
80
|
+
env.delete("CONTENT_TYPE")
|
81
|
+
end
|
82
|
+
rescue => error
|
83
|
+
puts "#{self.class.name} failed to modify the request! #{error.message}"
|
84
|
+
end
|
85
|
+
end
|
@@ -4,6 +4,8 @@ require_relative "sanitizer"
|
|
4
4
|
require_relative "state"
|
5
5
|
|
6
6
|
class TurboBoost::Commands::Runner
|
7
|
+
RESPONSE_HEADER = "TurboBoost-Command"
|
8
|
+
|
7
9
|
SUPPORTED_MEDIA_TYPES = {
|
8
10
|
"text/html" => true,
|
9
11
|
"text/vnd.turbo-boost.html" => true,
|
@@ -16,8 +18,8 @@ class TurboBoost::Commands::Runner
|
|
16
18
|
@controller = controller
|
17
19
|
end
|
18
20
|
|
19
|
-
def
|
20
|
-
@
|
21
|
+
def command_state
|
22
|
+
@command_state ||= begin
|
21
23
|
sgid = command_params[:signed_state]
|
22
24
|
value = TurboBoost::Commands::State.from_sgid_param(sgid) if sgid
|
23
25
|
value || TurboBoost::Commands::State.new
|
@@ -25,7 +27,7 @@ class TurboBoost::Commands::Runner
|
|
25
27
|
end
|
26
28
|
|
27
29
|
def command_requested?
|
28
|
-
|
30
|
+
controller.request.env.key?("turbo_boost_command") || controller.params.key?("turbo_boost_command")
|
29
31
|
end
|
30
32
|
|
31
33
|
def command_valid?
|
@@ -44,18 +46,19 @@ class TurboBoost::Commands::Runner
|
|
44
46
|
end
|
45
47
|
|
46
48
|
# validate csrf token
|
47
|
-
unless
|
49
|
+
unless valid_command_token?
|
48
50
|
raise TurboBoost::Commands::InvalidTokenError,
|
49
|
-
"Token mismatch! The token: #{
|
51
|
+
"Token mismatch! The token: #{client_command_token}` does not match the expected value of `#{server_command_token}`."
|
50
52
|
end
|
51
53
|
|
52
54
|
true
|
53
55
|
end
|
54
56
|
|
55
57
|
def command_params
|
56
|
-
return ActionController::Parameters.new
|
58
|
+
return ActionController::Parameters.new unless command_requested?
|
57
59
|
@command_params ||= begin
|
58
|
-
payload = parsed_command_params.
|
60
|
+
payload = parsed_command_params.transform_keys(&:underscore)
|
61
|
+
payload["element_attributes"]&.deep_transform_keys!(&:underscore)
|
59
62
|
ActionController::Parameters.new(payload).permit!
|
60
63
|
end
|
61
64
|
end
|
@@ -83,7 +86,7 @@ class TurboBoost::Commands::Runner
|
|
83
86
|
end
|
84
87
|
|
85
88
|
def command_instance
|
86
|
-
@command_instance ||= command_class&.new(controller,
|
89
|
+
@command_instance ||= command_class&.new(controller, command_state, command_params).tap do |instance|
|
87
90
|
instance&.add_observer self, :handle_command_event
|
88
91
|
end
|
89
92
|
end
|
@@ -108,6 +111,10 @@ class TurboBoost::Commands::Runner
|
|
108
111
|
!!command_instance&.succeeded?
|
109
112
|
end
|
110
113
|
|
114
|
+
def controller_action_allowed?
|
115
|
+
!controller_action_prevented?
|
116
|
+
end
|
117
|
+
|
111
118
|
def controller_action_prevented?
|
112
119
|
!!@controller_action_prevented
|
113
120
|
end
|
@@ -123,7 +130,8 @@ class TurboBoost::Commands::Runner
|
|
123
130
|
return if command_errored?
|
124
131
|
return if command_performing?
|
125
132
|
return if command_performed?
|
126
|
-
|
133
|
+
|
134
|
+
command_instance.resolve_state command_params[:changed_state]
|
127
135
|
command_instance.perform_with_callbacks command_method_name
|
128
136
|
end
|
129
137
|
|
@@ -136,17 +144,16 @@ class TurboBoost::Commands::Runner
|
|
136
144
|
render_response status: response_status
|
137
145
|
append_success_to_response
|
138
146
|
when TurboBoost::Commands::AbortError
|
139
|
-
render_response status: error.http_status_code,
|
147
|
+
render_response status: error.http_status_code, status_header: error.message
|
140
148
|
append_streams_to_response_body
|
141
149
|
when TurboBoost::Commands::PerformError
|
142
|
-
render_response status: error.http_status_code,
|
150
|
+
render_response status: error.http_status_code, status_header: error.message
|
143
151
|
append_error_to_response error
|
144
152
|
else
|
145
|
-
render_response status: :internal_server_error,
|
153
|
+
render_response status: :internal_server_error, status_header: error.message
|
146
154
|
append_error_to_response error
|
147
155
|
end
|
148
156
|
|
149
|
-
append_command_token_to_response_body
|
150
157
|
append_command_state_to_response_body
|
151
158
|
end
|
152
159
|
|
@@ -156,17 +163,16 @@ class TurboBoost::Commands::Runner
|
|
156
163
|
|
157
164
|
return if controller_action_prevented?
|
158
165
|
|
159
|
-
append_to_response_headers
|
160
|
-
append_command_token_to_response_body
|
161
166
|
append_command_state_to_response_body
|
167
|
+
append_to_response_headers if command_performed?
|
162
168
|
append_success_to_response if command_succeeded?
|
163
169
|
rescue => error
|
164
170
|
Rails.logger.error "TurboBoost::Commands::Runner failed to update the response! #{error.message}"
|
165
171
|
end
|
166
172
|
|
167
|
-
def render_response(html: "", status: nil,
|
168
|
-
controller.render html: html, layout: false, status: status || response_status
|
169
|
-
append_to_response_headers
|
173
|
+
def render_response(html: "", status: nil, status_header: nil)
|
174
|
+
controller.render html: html, layout: false, status: status || response_status # unless controller.performed?
|
175
|
+
append_to_response_headers status_header
|
170
176
|
end
|
171
177
|
|
172
178
|
def turbo_stream
|
@@ -191,40 +197,40 @@ class TurboBoost::Commands::Runner
|
|
191
197
|
private
|
192
198
|
|
193
199
|
def parsed_command_params
|
194
|
-
@parsed_command_params ||=
|
200
|
+
@parsed_command_params ||= begin
|
201
|
+
params = controller.request.env["turbo_boost_command"]
|
202
|
+
params ||= JSON.parse(controller.params["turbo_boost_command"])
|
203
|
+
params || {}
|
204
|
+
end
|
195
205
|
end
|
196
206
|
|
197
207
|
def content_sanitizer
|
198
208
|
TurboBoost::Commands::Sanitizer.instance
|
199
209
|
end
|
200
210
|
|
201
|
-
|
202
|
-
|
203
|
-
@new_token ||= SecureRandom.alphanumeric(13)
|
211
|
+
def new_command_token
|
212
|
+
@new_command_token ||= SecureRandom.alphanumeric(13)
|
204
213
|
end
|
205
214
|
|
206
|
-
|
207
|
-
|
208
|
-
nil
|
215
|
+
def client_command_token
|
216
|
+
command_params.dig(:client_state, :command_token)
|
209
217
|
end
|
210
218
|
|
211
|
-
|
212
|
-
|
213
|
-
command_params[:token].to_s
|
219
|
+
def server_command_token
|
220
|
+
command_state[:command_token]
|
214
221
|
end
|
215
222
|
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
true
|
223
|
+
def valid_command_token?
|
224
|
+
return true unless Rails.configuration.turbo_boost_commands.protect_from_forgery
|
225
|
+
return false unless client_command_token.present?
|
226
|
+
return false unless server_command_token.present?
|
227
|
+
server_command_token == message_verifier.verify(client_command_token, purpose: controller.request.session&.id)
|
228
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
229
|
+
false
|
224
230
|
end
|
225
231
|
|
226
232
|
def should_redirect?
|
227
|
-
return false if controller.request.
|
233
|
+
return false if controller.request.get?
|
228
234
|
controller.request.accepts.include? Mime::Type.lookup_by_extension(:turbo_stream)
|
229
235
|
end
|
230
236
|
|
@@ -235,12 +241,34 @@ class TurboBoost::Commands::Runner
|
|
235
241
|
|
236
242
|
def response_type
|
237
243
|
body = (controller.response_body.try(:join) || controller.response_body.to_s).strip
|
238
|
-
return :body if body.match?(/<\/\s*body/
|
239
|
-
return :frame if body.match?(/<\/\s*turbo-frame/
|
240
|
-
return :stream if body.match?(/<\/\s*turbo-stream/
|
244
|
+
return :body if body.match?(/<\/\s*body/io)
|
245
|
+
return :frame if body.match?(/<\/\s*turbo-frame/io)
|
246
|
+
return :stream if body.match?(/<\/\s*turbo-stream/io)
|
241
247
|
:unknown
|
242
248
|
end
|
243
249
|
|
250
|
+
# Indicates if a TurboStream template exists for the current action.
|
251
|
+
# Any template with the format of :turbo_boost or :turbo_stream format is considered a match.
|
252
|
+
# @return [Boolean] true if a TurboStream template exists, false otherwise
|
253
|
+
def turbo_stream_template_exists?
|
254
|
+
controller.lookup_context.exists? controller.action_name, controller.lookup_context.prefixes, formats: [:turbo_boost, :turbo_stream]
|
255
|
+
end
|
256
|
+
|
257
|
+
def rendering_strategy
|
258
|
+
# Use the replace strategy if the follow things are true:
|
259
|
+
#
|
260
|
+
# 1. The command was triggered by the WINDOW driver
|
261
|
+
# 2. After the command finishes, normal Rails mechanics resume (i.e. prevent_controller_action was not called)
|
262
|
+
# 3. There is NO TurboStream template for the current action (i.e. example.turbo_boost.erb, example.turbo_frame.erb)
|
263
|
+
#
|
264
|
+
# TODO: Revisit the "Replace" strategy after morph ships with Turbo 8
|
265
|
+
if command_params[:driver] == "window" && controller_action_allowed?
|
266
|
+
return "Replace" unless turbo_stream_template_exists?
|
267
|
+
end
|
268
|
+
|
269
|
+
"Append"
|
270
|
+
end
|
271
|
+
|
244
272
|
def append_success_to_response
|
245
273
|
append_success_event_to_response_body
|
246
274
|
append_streams_to_response_body
|
@@ -256,14 +284,16 @@ class TurboBoost::Commands::Runner
|
|
256
284
|
command_instance.turbo_streams.each { |stream| append_to_response_body stream }
|
257
285
|
end
|
258
286
|
|
259
|
-
def append_command_token_to_response_body
|
260
|
-
append_to_response_body turbo_stream.invoke("TurboBoost.Commands.token=", args: [new_token], camelize: false)
|
261
|
-
rescue => error
|
262
|
-
Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command token to the response! #{error.message}"
|
263
|
-
end
|
264
|
-
|
265
287
|
def append_command_state_to_response_body
|
266
|
-
|
288
|
+
# use the masked token for the client state
|
289
|
+
command_state[:command_token] = message_verifier.generate(new_command_token, purpose: controller.request.session&.id)
|
290
|
+
client_state = command_state.to_json
|
291
|
+
|
292
|
+
# use the unmasked token for the signed (server) state
|
293
|
+
command_state[:command_token] = new_command_token
|
294
|
+
signed_state = command_state.to_sgid_param
|
295
|
+
|
296
|
+
append_to_response_body turbo_stream.invoke("TurboBoost.State.initialize", args: [client_state, signed_state], camelize: false)
|
267
297
|
rescue => error
|
268
298
|
Rails.logger.error "TurboBoost::Commands::Runner failed to append the Command state to the response! #{error.message}"
|
269
299
|
end
|
@@ -316,10 +346,10 @@ class TurboBoost::Commands::Runner
|
|
316
346
|
|
317
347
|
html = case response_type
|
318
348
|
when :body
|
319
|
-
match = controller.response.body.match(/<\/\s*body/
|
349
|
+
match = controller.response.body.match(/<\/\s*body/io).to_s
|
320
350
|
controller.response.body.sub match, [sanitized_content, match].join
|
321
351
|
when :frame
|
322
|
-
match = controller.response.body.match(/<\/\s*turbo-frame/
|
352
|
+
match = controller.response.body.match(/<\/\s*turbo-frame/io).to_s
|
323
353
|
controller.response.body.sub match, [sanitized_content, match].join
|
324
354
|
else
|
325
355
|
[controller.response.body, sanitized_content].join
|
@@ -336,10 +366,15 @@ class TurboBoost::Commands::Runner
|
|
336
366
|
controller.response.set_header key.to_s, value.to_s
|
337
367
|
end
|
338
368
|
|
339
|
-
def append_to_response_headers(
|
369
|
+
def append_to_response_headers(status = nil)
|
340
370
|
return unless command_performed?
|
341
|
-
|
342
|
-
|
343
|
-
|
371
|
+
|
372
|
+
values = [
|
373
|
+
status || "#{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}".delete(","),
|
374
|
+
rendering_strategy,
|
375
|
+
command_name
|
376
|
+
]
|
377
|
+
|
378
|
+
append_response_header RESPONSE_HEADER, values.join(", ")
|
344
379
|
end
|
345
380
|
end
|