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.

@@ -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 }
@@ -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
- Object.values(events).forEach(name => {
14
- addEventListener(name, event => {
15
- if (logLevels[currentLevel].includes(event.type)) {
16
- const { target, detail } = event
17
- console[currentLevel](event.type, { target, detail })
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() {
@@ -1,16 +1,18 @@
1
- function replaceDocument(content) {
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
- function append(content) {
3
+ const append = content => {
13
4
  document.body.insertAdjacentHTML('beforeend', content)
14
5
  }
15
6
 
16
- export default { append, replaceDocument }
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 }
@@ -1,46 +1,37 @@
1
- import state from './state'
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 (frame) frameSources[frame.id] = frame.src
16
+ if (!header) return
27
17
 
28
- if (response.header('TurboBoost')) {
29
- if (response.statusCode < 200 || response.statusCode > 399) {
30
- const error = `Server returned a ${response.statusCode} status code! TurboBoost Commands require 2XX-3XX status codes.`
31
- dispatch(lifecycle.events.clientError, document, { detail: { ...event.detail, error } }, true)
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
- if (response.header('TurboBoost') === 'Append') {
35
- event.preventDefault()
36
- response.responseText.then(content => renderer.append(content))
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.turboBoostSrc = frameSources[frame.id] || frame.src || frame.dataset.turboBoostSrc
35
+ frame.dataset.src = frameSources[frame.id] || frame.src || frame.dataset.src
45
36
  delete frameSources[frame.id]
46
37
  })
@@ -1,9 +1,11 @@
1
- function build(urlString, payload = {}) {
1
+ const buildURL = path => {
2
2
  const a = document.createElement('a')
3
- a.href = urlString
4
- const url = new URL(a)
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 { build }
7
+ export default {
8
+ get commandInvocationURL() {
9
+ return buildURL('/turbo-boost-command-invocation')
10
+ }
11
+ }
@@ -1 +1 @@
1
- export default '0.1.2'
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 `perform` method, overridde in subclassed commands
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[:validate_client_token] = false
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
- config.turbo_boost_commands.precompile_assets = true
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 state
20
- @state ||= begin
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
- command_params.present?
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 valid_client_token?
49
+ unless valid_command_token?
48
50
  raise TurboBoost::Commands::InvalidTokenError,
49
- "Token mismatch! The token: #{client_token}` does not match the expected value of `#{server_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 if controller.params.keys.none?(/\A(tbc|turbo_boost_command)\z/o)
58
+ return ActionController::Parameters.new unless command_requested?
57
59
  @command_params ||= begin
58
- payload = parsed_command_params.deep_transform_keys(&:underscore)
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, state, command_params).tap do |instance|
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
- state.resolve command_params[:client_state]
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {"TurboBoost-Command-Status": error.message}
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, headers: {})
168
- controller.render html: html, layout: false, status: status || response_status
169
- append_to_response_headers headers.merge(TurboBoost: :Append)
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 ||= JSON.parse(controller.params[:tbc] || controller.params[:turbo_boost_command])
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
- # TODO: revisit command token validation
202
- def new_token
203
- @new_token ||= SecureRandom.alphanumeric(13)
211
+ def new_command_token
212
+ @new_command_token ||= SecureRandom.alphanumeric(13)
204
213
  end
205
214
 
206
- # TODO: revisit command token validation
207
- def server_token
208
- nil
215
+ def client_command_token
216
+ command_params.dig(:client_state, :command_token)
209
217
  end
210
218
 
211
- # TODO: revisit command token validation
212
- def client_token
213
- command_params[:token].to_s
219
+ def server_command_token
220
+ command_state[:command_token]
214
221
  end
215
222
 
216
- # TODO: revisit command token validation
217
- def valid_client_token?
218
- # return true unless Rails.configuration.turbo_boost_commands.validate_client_token
219
- # return false unless client_token.present?
220
- # return false unless message_verifier.valid_message?(client_token)
221
- # unmasked_client_token = message_verifier.verify(client_token)
222
- # unmasked_client_token == server_token
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.method.match?(/GET/i)
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/i)
239
- return :frame if body.match?(/<\/\s*turbo-frame/i)
240
- return :stream if body.match?(/<\/\s*turbo-stream/i)
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
- append_to_response_body turbo_stream.invoke("TurboBoost.State.initialize", args: [state.to_json, state.to_sgid_param], camelize: false)
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/i).to_s
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/i).to_s
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(headers = {})
369
+ def append_to_response_headers(status = nil)
340
370
  return unless command_performed?
341
- headers.each { |key, val| append_response_header key, val }
342
- append_response_header "TurboBoost-Command", command_name
343
- append_response_header "TurboBoost-Command-Status", "HTTP #{controller.response.status} #{TurboBoost::Commands::HTTP_STATUS_CODES[controller.response.status]}"
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