copy_tuner_client 2.1.2 → 2.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.
- checksums.yaml +4 -4
- data/.gitignore +5 -0
- data/.rubocop.yml +46 -1
- data/CHANGELOG.md +2 -235
- data/CLAUDE.md +1 -0
- data/Gemfile +1 -0
- data/README.md +42 -5
- data/copy_tuner_client.gemspec +3 -3
- data/gemfiles/8.0.gemfile +3 -3
- data/gemfiles/8.1.gemfile +3 -3
- data/gemfiles/main.gemfile +3 -3
- data/lib/copy_tuner_client/cache.rb +34 -27
- data/lib/copy_tuner_client/client.rb +35 -23
- data/lib/copy_tuner_client/configuration.rb +87 -40
- data/lib/copy_tuner_client/copyray/rewriter.rb +1 -1
- data/lib/copy_tuner_client/copyray.rb +1 -0
- data/lib/copy_tuner_client/copyray_middleware.rb +31 -24
- data/lib/copy_tuner_client/dotted_hash.rb +7 -7
- data/lib/copy_tuner_client/engine.rb +4 -6
- data/lib/copy_tuner_client/helper_extension.rb +58 -54
- data/lib/copy_tuner_client/i18n_backend.rb +12 -14
- data/lib/copy_tuner_client/i18n_compat.rb +2 -1
- data/lib/copy_tuner_client/poller.rb +3 -4
- data/lib/copy_tuner_client/prefixed_logger.rb +13 -12
- data/lib/copy_tuner_client/process_guard.rb +29 -26
- data/lib/copy_tuner_client/queue_with_timeout.rb +29 -19
- data/lib/copy_tuner_client/rails.rb +9 -8
- data/lib/copy_tuner_client/request_sync.rb +26 -24
- data/lib/copy_tuner_client/simple_form_extention.rb +10 -6
- data/lib/copy_tuner_client/translation_log.rb +10 -5
- data/lib/copy_tuner_client/version.rb +1 -1
- data/lib/copy_tuner_client.rb +4 -8
- data/lib/tasks/copy_tuner_client_tasks.rake +14 -16
- data/skills/copy-tuner-to-locales-cleanup/SKILL.md +5 -5
- data/skills/copy-tuner-to-locales-migrate-prefix/SKILL.md +69 -37
- data/skills/copy-tuner-to-locales-migrate-prefix/references/example-touchpoints.md +31 -10
- data/skills/copy-tuner-to-locales-migrate-prefix/scripts/migrate_prefix.rb +158 -13
- data/skills/copy-tuner-to-t-migrate/scripts/migrate_tt.rb +19 -17
- data/spec/copy_tuner_client/cache_spec.rb +63 -57
- data/spec/copy_tuner_client/client_spec.rb +82 -81
- data/spec/copy_tuner_client/configuration_spec.rb +380 -206
- data/spec/copy_tuner_client/copyray/marker_spec.rb +2 -1
- data/spec/copy_tuner_client/copyray/rewriter_spec.rb +15 -10
- data/spec/copy_tuner_client/copyray_middleware_spec.rb +9 -6
- data/spec/copy_tuner_client/copyray_spec.rb +8 -8
- data/spec/copy_tuner_client/dotted_hash_spec.rb +33 -33
- data/spec/copy_tuner_client/helper_extension_spec.rb +47 -44
- data/spec/copy_tuner_client/i18n_backend_spec.rb +144 -143
- data/spec/copy_tuner_client/poller_spec.rb +25 -24
- data/spec/copy_tuner_client/prefixed_logger_spec.rb +15 -12
- data/spec/copy_tuner_client/process_guard_spec.rb +27 -27
- data/spec/copy_tuner_client/request_sync_spec.rb +51 -58
- data/spec/copy_tuner_client/translation_log_spec.rb +38 -24
- data/spec/copy_tuner_client/warden_integration_spec.rb +95 -0
- data/spec/copy_tuner_client_spec.rb +5 -6
- data/spec/support/client_spec_helpers.rb +1 -1
- data/spec/support/defines_constants.rb +3 -34
- data/spec/support/fake_client.rb +1 -3
- data/spec/support/fake_copy_tuner_app.rb +54 -62
- data/spec/support/fake_html_safe_string.rb +2 -3
- data/spec/support/fake_logger.rb +22 -21
- data/spec/support/fake_passenger.rb +4 -6
- data/spec/support/fake_unicorn.rb +2 -4
- data/spec/support/middleware_stack.rb +38 -4
- data/spec/support/writing_cache.rb +4 -4
- metadata +2 -1
|
@@ -9,14 +9,16 @@ module CopyTunerClient
|
|
|
9
9
|
#
|
|
10
10
|
# A client is usually instantiated when {Configuration#apply} is called, and
|
|
11
11
|
# the application will not need to interact with it directly.
|
|
12
|
-
class Client
|
|
12
|
+
class Client # rubocop:disable Metrics/ClassLength
|
|
13
13
|
# These errors will be rescued when connecting CopyTuner.
|
|
14
|
-
HTTP_ERRORS = [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
HTTP_ERRORS = [
|
|
15
|
+
Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, EOFError,
|
|
16
|
+
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError,
|
|
17
|
+
Net::ProtocolError, SocketError, OpenSSL::SSL::SSLError,
|
|
18
|
+
Errno::ECONNREFUSED
|
|
19
|
+
].freeze
|
|
18
20
|
|
|
19
|
-
USER_AGENT = "copy_tuner_client #{CopyTunerClient::VERSION}"
|
|
21
|
+
USER_AGENT = "copy_tuner_client #{CopyTunerClient::VERSION}".freeze
|
|
20
22
|
|
|
21
23
|
# ETags を外部から取得可能にする
|
|
22
24
|
# @return [String, nil] 現在のETag値
|
|
@@ -36,9 +38,11 @@ module CopyTunerClient
|
|
|
36
38
|
@etag = nil
|
|
37
39
|
@downloaded_blurbs = {}
|
|
38
40
|
|
|
39
|
-
[
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
%i[
|
|
42
|
+
api_key host port public http_read_timeout
|
|
43
|
+
http_open_timeout secure logger ca_file s3_host download_cache_dir
|
|
44
|
+
].each do |option|
|
|
45
|
+
instance_variable_set("@#{option}", options[option])
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
@download_cache_dir.mkpath
|
|
@@ -59,19 +63,19 @@ module CopyTunerClient
|
|
|
59
63
|
connect(s3_host) do |http|
|
|
60
64
|
request = Net::HTTP::Get.new(uri(download_resource))
|
|
61
65
|
request['If-None-Match'] = @etag
|
|
62
|
-
log
|
|
66
|
+
log('Start downloading translations')
|
|
63
67
|
t = Time.now
|
|
64
68
|
response = http.request(request)
|
|
65
69
|
t_ms = ((Time.now - t) * 1000).to_i
|
|
66
70
|
downloaded = check(response)
|
|
67
71
|
if downloaded
|
|
68
72
|
# NOTE: Net::HTTPではgzipが透過的に扱われるため正確なファイルサイズや速度をログに出すのは難しい
|
|
69
|
-
log
|
|
73
|
+
log("Downloaded translations (#{t_ms}ms)")
|
|
70
74
|
@downloaded_blurbs = JSON.parse(response.body)
|
|
71
75
|
@etag = response['ETag']
|
|
72
76
|
last_download_path.write(JSON.pretty_generate(etag: @etag, downloaded_blurbs: @downloaded_blurbs))
|
|
73
77
|
else
|
|
74
|
-
log
|
|
78
|
+
log("No new translations (#{t_ms}ms)")
|
|
75
79
|
end
|
|
76
80
|
|
|
77
81
|
yield(@downloaded_blurbs) if downloaded || cache_fallback
|
|
@@ -83,9 +87,10 @@ module CopyTunerClient
|
|
|
83
87
|
# @raise [ConnectionError] if the connection fails
|
|
84
88
|
def upload(data)
|
|
85
89
|
connect(host) do |http|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
headers = { 'Content-Type' => 'application/json', 'User-Agent' => USER_AGENT }
|
|
91
|
+
response = http.post(uri('draft_blurbs'), data.to_json, headers)
|
|
92
|
+
check(response)
|
|
93
|
+
log('Uploaded missing translations')
|
|
89
94
|
end
|
|
90
95
|
end
|
|
91
96
|
|
|
@@ -94,15 +99,22 @@ module CopyTunerClient
|
|
|
94
99
|
def deploy
|
|
95
100
|
connect(host) do |http|
|
|
96
101
|
response = http.post(uri('deploys'), '', 'User-Agent' => USER_AGENT)
|
|
97
|
-
check
|
|
98
|
-
log
|
|
102
|
+
check(response)
|
|
103
|
+
log('Deployed')
|
|
99
104
|
end
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
private
|
|
103
108
|
|
|
104
|
-
attr_reader :host,
|
|
105
|
-
|
|
109
|
+
attr_reader :host,
|
|
110
|
+
:port,
|
|
111
|
+
:api_key,
|
|
112
|
+
:http_read_timeout,
|
|
113
|
+
:http_open_timeout,
|
|
114
|
+
:secure,
|
|
115
|
+
:logger,
|
|
116
|
+
:ca_file,
|
|
117
|
+
:s3_host
|
|
106
118
|
|
|
107
119
|
def public?
|
|
108
120
|
@public
|
|
@@ -130,7 +142,7 @@ module CopyTunerClient
|
|
|
130
142
|
cache = JSON.parse(pathname.read.to_s).transform_keys(&:to_sym)
|
|
131
143
|
@etag = cache[:etag]
|
|
132
144
|
@downloaded_blurbs = cache[:downloaded_blurbs]
|
|
133
|
-
log
|
|
145
|
+
log("Loaded cache data from #{pathname}")
|
|
134
146
|
rescue JSON::JSONError, Errno::ENOENT, Errno::ENOTDIR
|
|
135
147
|
nil
|
|
136
148
|
end
|
|
@@ -144,9 +156,9 @@ module CopyTunerClient
|
|
|
144
156
|
http.ca_file = ca_file
|
|
145
157
|
|
|
146
158
|
begin
|
|
147
|
-
yield
|
|
148
|
-
rescue *HTTP_ERRORS =>
|
|
149
|
-
raise ConnectionError, "#{
|
|
159
|
+
yield(http)
|
|
160
|
+
rescue *HTTP_ERRORS => e
|
|
161
|
+
raise ConnectionError, "#{e.class.name}: #{e.message}"
|
|
150
162
|
end
|
|
151
163
|
end
|
|
152
164
|
|
|
@@ -12,14 +12,16 @@ module CopyTunerClient
|
|
|
12
12
|
# Used to set up and modify settings for the client.
|
|
13
13
|
class Configuration # rubocop:disable Metrics/ClassLength
|
|
14
14
|
# These options will be present in the Hash returned by {#to_hash}.
|
|
15
|
-
OPTIONS = %i[
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
OPTIONS = %i[
|
|
16
|
+
api_key development_environments environment_name host
|
|
17
|
+
http_open_timeout http_read_timeout client_name client_url
|
|
18
|
+
client_version port protocol proxy_host proxy_pass
|
|
19
|
+
proxy_port proxy_user secure polling_delay sync_interval
|
|
20
|
+
sync_interval_staging sync_ignore_path_regex logger
|
|
21
|
+
framework middleware disable_middleware disable_test_translation
|
|
22
|
+
ca_file local_first_key_regexp s3_host locales ignored_keys ignored_key_handler
|
|
23
|
+
download_cache_dir
|
|
24
|
+
].freeze
|
|
23
25
|
|
|
24
26
|
# NOTE: Rails 標準ロケールで非文字列値(precision: Integer, significant: Boolean,
|
|
25
27
|
# strip_insignificant_zeros: Boolean)を含むのは number.*.format 配下のみ。store_item が文字列しか
|
|
@@ -182,6 +184,7 @@ module CopyTunerClient
|
|
|
182
184
|
self.local_first_key_regexp = nil
|
|
183
185
|
self.project_id = nil
|
|
184
186
|
self.download_cache_dir = Pathname.new(Dir.pwd).join('tmp', 'cache', 'copy_tuner_client')
|
|
187
|
+
self.middleware_position = default_middleware_position
|
|
185
188
|
|
|
186
189
|
@applied = false
|
|
187
190
|
end
|
|
@@ -200,7 +203,7 @@ module CopyTunerClient
|
|
|
200
203
|
base_options = { public: public?, upload_disabled: upload_disabled? }
|
|
201
204
|
|
|
202
205
|
OPTIONS.inject(base_options) do |hash, option|
|
|
203
|
-
hash.merge
|
|
206
|
+
hash.merge(option.to_sym => public_send(option))
|
|
204
207
|
end
|
|
205
208
|
end
|
|
206
209
|
|
|
@@ -209,7 +212,7 @@ module CopyTunerClient
|
|
|
209
212
|
# @param [Hash] hash A set of configuration options that will take precedence over the defaults
|
|
210
213
|
# @return [Hash] the merged configuration hash
|
|
211
214
|
def merge(hash)
|
|
212
|
-
to_hash.merge
|
|
215
|
+
to_hash.merge(hash)
|
|
213
216
|
end
|
|
214
217
|
|
|
215
218
|
# Determines if the published or draft content will be used
|
|
@@ -222,7 +225,7 @@ module CopyTunerClient
|
|
|
222
225
|
# Determines if the content will be editable
|
|
223
226
|
# @return [Boolean] Returns +true+ if in a development environment, +false+ otherwise.
|
|
224
227
|
def development?
|
|
225
|
-
development_environments.include?
|
|
228
|
+
development_environments.include?(environment_name)
|
|
226
229
|
end
|
|
227
230
|
|
|
228
231
|
def enable_middleware?
|
|
@@ -252,15 +255,11 @@ module CopyTunerClient
|
|
|
252
255
|
# This creates the {I18nBackend} and puts them together.
|
|
253
256
|
#
|
|
254
257
|
# When {#test?} returns +false+, the poller will be started.
|
|
255
|
-
def apply # rubocop:disable Metrics/AbcSize
|
|
258
|
+
def apply # rubocop:disable Metrics/AbcSize
|
|
256
259
|
# NOTE: project_id は必須。未設定なら apply 時点で明示的に失敗させる
|
|
257
260
|
validate_project_id!
|
|
258
261
|
|
|
259
|
-
self.locales ||=
|
|
260
|
-
::Rails.application.config.i18n.available_locales.presence || Array(::Rails.application.config.i18n.default_locale)
|
|
261
|
-
else
|
|
262
|
-
[:en]
|
|
263
|
-
end
|
|
262
|
+
self.locales ||= default_locales
|
|
264
263
|
|
|
265
264
|
self.client ||= Client.new(to_hash)
|
|
266
265
|
self.cache ||= Cache.new(client, to_hash)
|
|
@@ -268,31 +267,12 @@ module CopyTunerClient
|
|
|
268
267
|
process_guard = ProcessGuard.new(cache, @poller, to_hash)
|
|
269
268
|
I18n.backend = I18nBackend.new(cache)
|
|
270
269
|
|
|
271
|
-
|
|
272
|
-
logger.info 'Using copytuner sync middleware'
|
|
273
|
-
request_sync_options = { poller: @poller, cache:, interval: sync_interval, ignore_regex: sync_ignore_path_regex }
|
|
274
|
-
if middleware_position.is_a?(Hash) && middleware_position[:before]
|
|
275
|
-
middleware.insert_before middleware_position[:before], RequestSync, request_sync_options
|
|
276
|
-
middleware.insert_before middleware_position[:before], CopyTunerClient::CopyrayMiddleware
|
|
277
|
-
elsif middleware_position.is_a?(Hash) && middleware_position[:after]
|
|
278
|
-
middleware.insert_after middleware_position[:after], RequestSync, request_sync_options
|
|
279
|
-
middleware.insert_after middleware_position[:after], CopyTunerClient::CopyrayMiddleware
|
|
280
|
-
else
|
|
281
|
-
middleware.use RequestSync, request_sync_options
|
|
282
|
-
middleware.use CopyTunerClient::CopyrayMiddleware
|
|
283
|
-
end
|
|
284
|
-
else
|
|
285
|
-
logger.info '[[[Warn]]] Not using copytuner sync middleware' unless middleware
|
|
286
|
-
end
|
|
270
|
+
setup_middleware
|
|
287
271
|
|
|
288
272
|
@applied = true
|
|
289
|
-
|
|
290
|
-
logger.info "Environment Info: #{environment_info}"
|
|
291
|
-
logger.info "Available locales: #{self.locales.join(' ')}"
|
|
273
|
+
log_applied
|
|
292
274
|
|
|
293
|
-
unless test?
|
|
294
|
-
process_guard.start
|
|
295
|
-
end
|
|
275
|
+
process_guard.start unless test?
|
|
296
276
|
|
|
297
277
|
unless test? && disable_test_translation
|
|
298
278
|
logger.info 'Download translation now'
|
|
@@ -348,7 +328,7 @@ module CopyTunerClient
|
|
|
348
328
|
validate_project_id!
|
|
349
329
|
|
|
350
330
|
path = "/projects/#{project_id}"
|
|
351
|
-
URI::Generic.build(scheme:
|
|
331
|
+
URI::Generic.build(scheme: protocol, host:, port: port.to_i, path:).to_s
|
|
352
332
|
end
|
|
353
333
|
|
|
354
334
|
# locale を除いたキーが local_first_key_regexp にマッチするかを返す。
|
|
@@ -369,6 +349,73 @@ module CopyTunerClient
|
|
|
369
349
|
|
|
370
350
|
private
|
|
371
351
|
|
|
352
|
+
def default_locales
|
|
353
|
+
if defined?(::Rails)
|
|
354
|
+
rails_i18n = ::Rails.application.config.i18n
|
|
355
|
+
rails_i18n.available_locales.presence || Array(rails_i18n.default_locale)
|
|
356
|
+
else
|
|
357
|
+
[:en]
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# throw :warden は Warden::Manager の catch(:warden) までスタックを巻き戻すため、
|
|
362
|
+
# Warden より内側の middleware は @app.call の戻り値を受け取れず、Devise::FailureApp の
|
|
363
|
+
# 応答がマーカー除去を経ずにブラウザへ届いてしまう。これを避けるため Warden の直前を既定位置にする。
|
|
364
|
+
#
|
|
365
|
+
# 判定に Devise の有無も見るのは、warden を require するだけで Warden::Manager を
|
|
366
|
+
# スタックに積まない gem(authtrail 等)が存在するため。定数の有無だけで決めると
|
|
367
|
+
# そうしたアプリで insert_before が対象を見つけられず起動時例外になる。
|
|
368
|
+
# スタックへ積むのは Devise の railtie(config.app_middleware.use)である。
|
|
369
|
+
def default_middleware_position
|
|
370
|
+
return nil unless defined?(::Warden::Manager) && defined?(::Devise)
|
|
371
|
+
|
|
372
|
+
{ before: ::Warden::Manager }
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def setup_middleware
|
|
376
|
+
if enable_middleware?
|
|
377
|
+
logger.info 'Using copytuner sync middleware'
|
|
378
|
+
insert_middleware
|
|
379
|
+
else
|
|
380
|
+
logger.info '[[[Warn]]] Not using copytuner sync middleware' unless middleware
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def log_applied
|
|
385
|
+
logger.info "Client #{VERSION} ready (s3_download)"
|
|
386
|
+
logger.info "Environment Info: #{environment_info}"
|
|
387
|
+
logger.info "Available locales: #{locales.join(' ')}"
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def insert_middleware
|
|
391
|
+
# NOTE: 値の nil を除外するのは、{ before: SomeClass if cond } のように条件次第で nil が入る
|
|
392
|
+
# 書き方で従来は末尾 use にフォールバックしていた挙動を保つため(キーの有無だけで分岐すると
|
|
393
|
+
# insert_before(nil) が対象を見つけられず例外になる)。
|
|
394
|
+
case middleware_position
|
|
395
|
+
in { before: target } if target
|
|
396
|
+
middleware.insert_before(target, RequestSync, request_sync_options)
|
|
397
|
+
middleware.insert_before(target, CopyTunerClient::CopyrayMiddleware)
|
|
398
|
+
in { after: target } if target
|
|
399
|
+
# NOTE: insert_after(index, *) は insert(index + 1, *) を呼ぶため、同じ対象へ 2 回挿入すると
|
|
400
|
+
# 後から挿入した方が対象に近い位置に来て順序が反転する。逆順で呼ぶことで
|
|
401
|
+
# 外側→内側が RequestSync → CopyrayMiddleware に揃う。
|
|
402
|
+
middleware.insert_after(target, CopyTunerClient::CopyrayMiddleware)
|
|
403
|
+
middleware.insert_after(target, RequestSync, request_sync_options)
|
|
404
|
+
else
|
|
405
|
+
middleware.use(RequestSync, request_sync_options)
|
|
406
|
+
middleware.use(CopyTunerClient::CopyrayMiddleware)
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def request_sync_options
|
|
411
|
+
{
|
|
412
|
+
poller: @poller,
|
|
413
|
+
cache:,
|
|
414
|
+
interval: sync_interval,
|
|
415
|
+
ignore_regex: sync_ignore_path_regex,
|
|
416
|
+
}
|
|
417
|
+
end
|
|
418
|
+
|
|
372
419
|
# project_id は必須。未設定なら明示的に失敗させる。
|
|
373
420
|
# apply(起動時の全体検証)と project_url(apply を経ない経路へのセーフネット)の両方から呼ぶ。
|
|
374
421
|
def validate_project_id!
|
|
@@ -38,7 +38,7 @@ module CopyTunerClient
|
|
|
38
38
|
# NOTE: 閾値超は Nokogiri を通さず可視トークン除去のみ。skipped=true で編集導線を諦めた旨を伝える。
|
|
39
39
|
return [strip_markers(scannable), true] if scannable.bytesize > MAX_REWRITE_BYTESIZE
|
|
40
40
|
|
|
41
|
-
[rewrite_with_nokogiri(scannable, fragment:
|
|
41
|
+
[rewrite_with_nokogiri(scannable, fragment:), false]
|
|
42
42
|
rescue StandardError => e
|
|
43
43
|
# NOTE: Copyray は開発支援機能なので、壊れた HTML 等で Nokogiri 処理が落ちても
|
|
44
44
|
# ページを 500 にしない。data-copyray-key 付与(編集導線)は諦め、最低限可視トークンだけ除去する。
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require 'copy_tuner_client/copyray/rewriter'
|
|
4
4
|
|
|
5
5
|
module CopyTunerClient
|
|
6
|
+
# レスポンス HTML を Rewriter に通し、マーカートークンを data-copyray-key 属性へ変換する Rack middleware
|
|
6
7
|
class CopyrayMiddleware
|
|
7
8
|
def initialize(app)
|
|
8
9
|
@app = app
|
|
@@ -11,26 +12,8 @@ module CopyTunerClient
|
|
|
11
12
|
def call(env)
|
|
12
13
|
CopyTunerClient::TranslationLog.clear
|
|
13
14
|
status, headers, response = @app.call(env)
|
|
14
|
-
if rewritable?(status, headers) && body = response_body(response)
|
|
15
|
-
|
|
16
|
-
# NOTE: CSS/JS 挿入の前に Rewriter を通す。serialize 後も </body> は必ず出力されるので
|
|
17
|
-
# append_to_html_body の rindex は機能し、CSS/JS タグはトークン非含有なので二重処理も起きない。
|
|
18
|
-
# NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
|
|
19
|
-
# JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
|
|
20
|
-
# NOTE: turbo stream はページ断片なので fragment パーサで走査する(html/body ラッパを付けない)。
|
|
21
|
-
turbo_stream = turbo_stream?(headers)
|
|
22
|
-
body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body, fragment: turbo_stream)
|
|
23
|
-
# NOTE: ブートストラップ JS はフルページ読み込み時に一度だけ挿入すればよい。turbo stream 断片には
|
|
24
|
-
# 挿入先の </body> も無く、既に初期化済みのページへマージされるだけなので挿入しない。
|
|
25
|
-
body = append_js(body, csp_nonce, skipped: skipped) unless turbo_stream
|
|
26
|
-
content_length = body.bytesize.to_s
|
|
27
|
-
headers['Content-Length'] = content_length
|
|
28
|
-
# maintains compatibility with other middlewares
|
|
29
|
-
if defined?(ActionDispatch::Response::RackBody) && response.is_a?(ActionDispatch::Response::RackBody)
|
|
30
|
-
ActionDispatch::Response.new(status, headers, [body]).to_a
|
|
31
|
-
else
|
|
32
|
-
[status, headers, [body]]
|
|
33
|
-
end
|
|
15
|
+
if rewritable?(status, headers) && (body = response_body(response))
|
|
16
|
+
rewrite_response(env, status, headers, body, response)
|
|
34
17
|
else
|
|
35
18
|
[status, headers, response]
|
|
36
19
|
end
|
|
@@ -38,6 +21,30 @@ module CopyTunerClient
|
|
|
38
21
|
|
|
39
22
|
private
|
|
40
23
|
|
|
24
|
+
def rewrite_response(env, status, headers, body, response)
|
|
25
|
+
csp_nonce =
|
|
26
|
+
env.fetch('action_dispatch.content_security_policy_nonce') do
|
|
27
|
+
env['secure_headers_content_security_policy_nonce']
|
|
28
|
+
end
|
|
29
|
+
# NOTE: CSS/JS 挿入の前に Rewriter を通す。serialize 後も </body> は必ず出力されるので
|
|
30
|
+
# append_to_html_body の rindex は機能し、CSS/JS タグはトークン非含有なので二重処理も起きない。
|
|
31
|
+
# NOTE: skipped は data-copyray-key を付与できなかったこと(巨大DOM/Nokogiri例外)を表す。
|
|
32
|
+
# JS にこれを伝え、オーバーレイ非対応である旨をツールバーで案内させる。
|
|
33
|
+
# NOTE: turbo stream はページ断片なので fragment パーサで走査する(html/body ラッパを付けない)。
|
|
34
|
+
turbo_stream = turbo_stream?(headers)
|
|
35
|
+
body, skipped = CopyTunerClient::Copyray::Rewriter.rewrite(body, fragment: turbo_stream)
|
|
36
|
+
# NOTE: ブートストラップ JS はフルページ読み込み時に一度だけ挿入すればよい。turbo stream 断片には
|
|
37
|
+
# 挿入先の </body> も無く、既に初期化済みのページへマージされるだけなので挿入しない。
|
|
38
|
+
body = append_js(body, csp_nonce, skipped:) unless turbo_stream
|
|
39
|
+
headers['Content-Length'] = body.bytesize.to_s
|
|
40
|
+
# maintains compatibility with other middlewares
|
|
41
|
+
if defined?(ActionDispatch::Response::RackBody) && response.is_a?(ActionDispatch::Response::RackBody)
|
|
42
|
+
ActionDispatch::Response.new(status, headers, [body]).to_a
|
|
43
|
+
else
|
|
44
|
+
[status, headers, [body]]
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
41
48
|
def helpers
|
|
42
49
|
ActionController::Base.helpers
|
|
43
50
|
end
|
|
@@ -57,7 +64,8 @@ module CopyTunerClient
|
|
|
57
64
|
keysSkipped: #{skipped},
|
|
58
65
|
}
|
|
59
66
|
SCRIPT
|
|
60
|
-
|
|
67
|
+
tag = helpers.javascript_include_tag('copytuner', type: 'module', crossorigin: 'anonymous', nonce: csp_nonce)
|
|
68
|
+
append_to_html_body(html, tag)
|
|
61
69
|
end
|
|
62
70
|
|
|
63
71
|
def append_to_html_body(html, content)
|
|
@@ -65,7 +73,7 @@ module CopyTunerClient
|
|
|
65
73
|
return html unless html.include?('</body>')
|
|
66
74
|
|
|
67
75
|
position = html.rindex('</body>')
|
|
68
|
-
html.insert(position, content
|
|
76
|
+
html.insert(position, "#{content}\n")
|
|
69
77
|
end
|
|
70
78
|
|
|
71
79
|
def file?(headers)
|
|
@@ -80,8 +88,7 @@ module CopyTunerClient
|
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def turbo_stream?(headers)
|
|
83
|
-
headers['Content-Type']
|
|
84
|
-
headers['Content-Type'].include?('text/vnd.turbo-stream.html')
|
|
91
|
+
headers['Content-Type']&.include?('text/vnd.turbo-stream.html')
|
|
85
92
|
end
|
|
86
93
|
|
|
87
94
|
def response_body(response)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
module CopyTunerClient
|
|
2
|
+
# `a.b.c` 形式のドット区切りキーを持つ Hash とネストした Hash を相互変換する
|
|
2
3
|
module DottedHash
|
|
3
4
|
def to_h(dotted_hash)
|
|
4
5
|
hash = {}
|
|
@@ -15,18 +16,17 @@ module CopyTunerClient
|
|
|
15
16
|
|
|
16
17
|
all_keys.each_with_index do |key, index|
|
|
17
18
|
prefix = "#{key}."
|
|
18
|
-
conflict_keys =
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
conflict_keys =
|
|
20
|
+
((index + 1)..Float::INFINITY)
|
|
21
|
+
.take_while { |i| all_keys[i]&.start_with?(prefix) }
|
|
22
|
+
.map { |i| all_keys[i] }
|
|
21
23
|
|
|
22
|
-
if conflict_keys.present?
|
|
23
|
-
results[key] = conflict_keys
|
|
24
|
-
end
|
|
24
|
+
results[key] = conflict_keys if conflict_keys.present?
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
results
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
module_function :to_h, :conflict_keys
|
|
30
|
+
module_function :to_h, :conflict_keys
|
|
31
31
|
end
|
|
32
32
|
end
|
|
@@ -5,11 +5,11 @@ require 'copy_tuner_client/helper_extension'
|
|
|
5
5
|
module CopyTunerClient
|
|
6
6
|
# Connects to integration points for Rails 3 applications
|
|
7
7
|
class Engine < ::Rails::Engine
|
|
8
|
-
initializer :initialize_copy_tuner_rails, :
|
|
8
|
+
initializer :initialize_copy_tuner_rails, before: :load_config_initializers do |_app|
|
|
9
9
|
CopyTunerClient::Rails.initialize
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
initializer :initialize_copy_tuner_hook_methods, :
|
|
12
|
+
initializer :initialize_copy_tuner_hook_methods, after: :load_config_initializers do |_app|
|
|
13
13
|
ActiveSupport.on_load(:action_view) do
|
|
14
14
|
CopyTunerClient::HelperExtension.hook_translation_helper(
|
|
15
15
|
ActionView::Helpers::TranslationHelper,
|
|
@@ -17,14 +17,12 @@ module CopyTunerClient
|
|
|
17
17
|
)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
-
if CopyTunerClient.configuration.enable_middleware?
|
|
21
|
-
CopyTunerClient::TranslationLog.install_hook
|
|
22
|
-
end
|
|
20
|
+
CopyTunerClient::TranslationLog.install_hook if CopyTunerClient.configuration.enable_middleware?
|
|
23
21
|
|
|
24
22
|
require 'copy_tuner_client/simple_form_extention'
|
|
25
23
|
end
|
|
26
24
|
|
|
27
|
-
initializer
|
|
25
|
+
initializer 'copy_tuner.assets.precompile', group: :all do |app|
|
|
28
26
|
app.config.assets.precompile += ['copytuner.js']
|
|
29
27
|
end
|
|
30
28
|
end
|
|
@@ -1,67 +1,71 @@
|
|
|
1
1
|
module CopyTunerClient
|
|
2
|
+
# ActionView の translate/t を alias_method で差し替え、Copyray のオーバーレイマーカーを注入する
|
|
2
3
|
module HelperExtension
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
# NOTE: class_eval ブロック内で def すると、ブロック内メソッドの複雑さが
|
|
5
|
+
# hook_translation_helper 自体の Metrics/AbcSize としてカウントされてしまうため、
|
|
6
|
+
# メソッド本体は独立したモジュールに切り出して include する。
|
|
7
|
+
module CopyrayCommentInjection
|
|
8
|
+
def translate_with_copyray_comment(key, **options)
|
|
9
|
+
source = translate_without_copyray_comment(key, **options)
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
return source
|
|
11
|
-
end
|
|
11
|
+
return source if controller && CopyTunerClient::Rails.controller_of_rails_engine?(controller)
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
# TODO: test
|
|
14
|
+
# NOTE: default引数が設定されている場合は、copytunerキャッシュの値をI18n.t呼び出しにより上書きしている
|
|
15
|
+
# SEE: https://github.com/rails/rails/blob/6c43ebc220428ce9fc9569c2e5df90a38a4fc4e4/actionview/lib/action_view/helpers/translation_helper.rb#L82
|
|
16
|
+
if options.key?(:default)
|
|
17
|
+
I18n.t(key.to_s.first == '.' ? scope_key_by_partial(key) : key, **options)
|
|
18
|
+
end
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
# NOTE: マーカーは HTML コメントとしてブラウザに無視されつつ Copyray オーバーレイのキー特定に
|
|
21
|
+
# 使われる。HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではコメントが文字列として
|
|
22
|
+
# 出力に混入してしまうため、それらの経路には注入しない。default 引数による初期値登録は維持する
|
|
23
|
+
# 必要があるため、このガードは初期値登録(上の I18n.t 呼び出し)より後に置く。
|
|
24
|
+
return source unless copyray_injectable?
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if key.to_s.first == '.'
|
|
33
|
-
scope_key_by_partial(key)
|
|
34
|
-
else
|
|
35
|
-
# NOTE: locale prefix無しのkeyが必要のためこうしている
|
|
36
|
-
I18n.normalize_keys(nil, key, scope, separator).compact.join(separator)
|
|
37
|
-
end
|
|
38
|
-
CopyTunerClient::Copyray.augment_template(source, scope_key)
|
|
39
|
-
end
|
|
40
|
-
end
|
|
26
|
+
if CopyTunerClient.configuration.disable_copyray_comment_injection
|
|
27
|
+
source
|
|
28
|
+
else
|
|
29
|
+
CopyTunerClient::Copyray.augment_template(source, copyray_scope_key(key, options))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
41
32
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
# lookup_context.formats のような ActionView の内部実装には依存させない(Rails バージョン間で壊れうるため)。
|
|
45
|
-
def copyray_injectable?
|
|
46
|
-
current_controller = controller
|
|
47
|
-
return false if current_controller.nil?
|
|
33
|
+
def copyray_scope_key(key, options)
|
|
34
|
+
return scope_key_by_partial(key) if key.to_s.first == '.'
|
|
48
35
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
36
|
+
separator = options.fetch(:separator, I18n.default_separator)
|
|
37
|
+
# NOTE: locale prefix無しのkeyが必要のためこうしている
|
|
38
|
+
I18n.normalize_keys(nil, key, options[:scope], separator).compact.join(separator)
|
|
39
|
+
end
|
|
40
|
+
private :copyray_scope_key
|
|
52
41
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
# NOTE: HTML 以外の経路(メール本文・render :json・CSV/PDF など)ではマーカーが文字列として
|
|
43
|
+
# 出力に混入するため注入しない。判定には controller.request.format を使い、@current_template.format /
|
|
44
|
+
# lookup_context.formats のような ActionView の内部実装には依存させない(Rails バージョン間で壊れうるため)。
|
|
45
|
+
def copyray_injectable?
|
|
46
|
+
current_controller = controller
|
|
47
|
+
return false if current_controller.nil?
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
49
|
+
# NOTE: mailer は request を持たず request.format で判定できない。かつメール本文への
|
|
50
|
+
# マーカー混入は実害が大きいため、controller の型で明示除外する。
|
|
51
|
+
return false if defined?(ActionMailer::Base) && current_controller.is_a?(ActionMailer::Base)
|
|
52
|
+
|
|
53
|
+
# NOTE: request が無い/format が html でない経路には注入しない。&. と || false で
|
|
54
|
+
# request 不在時も安全に false を返す。
|
|
55
|
+
current_controller.request&.format&.html? || false
|
|
56
|
+
end
|
|
57
|
+
private :copyray_injectable?
|
|
58
|
+
end
|
|
59
|
+
private_constant :CopyrayCommentInjection
|
|
60
|
+
|
|
61
|
+
def self.hook_translation_helper(mod, middleware_enabled:)
|
|
62
|
+
return unless middleware_enabled
|
|
63
|
+
|
|
64
|
+
mod.include(CopyrayCommentInjection)
|
|
65
|
+
mod.class_eval do
|
|
66
|
+
alias_method :translate_without_copyray_comment, :translate
|
|
67
|
+
alias_method :translate, :translate_with_copyray_comment
|
|
68
|
+
alias_method :t, :translate
|
|
65
69
|
end
|
|
66
70
|
end
|
|
67
71
|
end
|