Merge branch 'main' into less-initializers
This commit is contained in:
commit
fd41ea1f2d
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,7 +2,6 @@
|
|||||||
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
|
# Check out https://help.github.com/articles/ignoring-files for how to set that up.
|
||||||
|
|
||||||
.Gemfile
|
.Gemfile
|
||||||
.byebug_history
|
|
||||||
.ruby-version
|
.ruby-version
|
||||||
/*/doc/
|
/*/doc/
|
||||||
/*/test/tmp/
|
/*/test/tmp/
|
||||||
|
10
.rubocop.yml
10
.rubocop.yml
@ -1,4 +1,5 @@
|
|||||||
require:
|
require:
|
||||||
|
- rubocop-minitest
|
||||||
- rubocop-packaging
|
- rubocop-packaging
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
@ -49,6 +50,9 @@ Layout/CaseIndentation:
|
|||||||
Layout/ClosingHeredocIndentation:
|
Layout/ClosingHeredocIndentation:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Layout/ClosingParenthesisIndentation:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
# Align comments with method definitions.
|
# Align comments with method definitions.
|
||||||
Layout/CommentIndentation:
|
Layout/CommentIndentation:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
@ -139,6 +143,9 @@ Style/DefWithParentheses:
|
|||||||
Style/MethodDefParentheses:
|
Style/MethodDefParentheses:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Style/ExplicitBlockArgument:
|
||||||
|
Enabled: true
|
||||||
|
|
||||||
Style/FrozenStringLiteralComment:
|
Style/FrozenStringLiteralComment:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
EnforcedStyle: always
|
EnforcedStyle: always
|
||||||
@ -288,3 +295,6 @@ Performance/DeletePrefix:
|
|||||||
|
|
||||||
Performance/DeleteSuffix:
|
Performance/DeleteSuffix:
|
||||||
Enabled: true
|
Enabled: true
|
||||||
|
|
||||||
|
Minitest/UnreachableAssertion:
|
||||||
|
Enabled: true
|
||||||
|
22
Gemfile
22
Gemfile
@ -13,9 +13,12 @@ gem "capybara", ">= 3.26"
|
|||||||
gem "selenium-webdriver", ">= 4.0.0.alpha7"
|
gem "selenium-webdriver", ">= 4.0.0.alpha7"
|
||||||
|
|
||||||
gem "rack-cache", "~> 1.2"
|
gem "rack-cache", "~> 1.2"
|
||||||
gem "sass-rails"
|
gem "stimulus-rails"
|
||||||
gem "turbolinks", "~> 5"
|
gem "turbo-rails"
|
||||||
gem "webpacker", "~> 5.0", require: ENV["SKIP_REQUIRE_WEBPACKER"] != "true"
|
gem "jsbundling-rails"
|
||||||
|
gem "cssbundling-rails"
|
||||||
|
gem "importmap-rails"
|
||||||
|
gem "tailwindcss-rails"
|
||||||
# require: false so bcrypt is loaded only when has_secure_password is used.
|
# require: false so bcrypt is loaded only when has_secure_password is used.
|
||||||
# This is to avoid Active Model (and by extension the entire framework)
|
# This is to avoid Active Model (and by extension the entire framework)
|
||||||
# being dependent on a binary library.
|
# being dependent on a binary library.
|
||||||
@ -23,13 +26,14 @@ gem "bcrypt", "~> 3.1.11", require: false
|
|||||||
|
|
||||||
# This needs to be with require false to avoid it being automatically loaded by
|
# This needs to be with require false to avoid it being automatically loaded by
|
||||||
# sprockets.
|
# sprockets.
|
||||||
gem "uglifier", ">= 1.3.0", require: false
|
gem "terser", ">= 1.1.4", require: false
|
||||||
|
|
||||||
# Explicitly avoid 1.x that doesn't support Ruby 2.4+
|
# Explicitly avoid 1.x that doesn't support Ruby 2.4+
|
||||||
gem "json", ">= 2.0.0"
|
gem "json", ">= 2.0.0"
|
||||||
|
|
||||||
group :rubocop do
|
group :rubocop do
|
||||||
gem "rubocop", ">= 0.90", require: false
|
gem "rubocop", ">= 0.90", require: false
|
||||||
|
gem "rubocop-minitest", require: false
|
||||||
gem "rubocop-packaging", require: false
|
gem "rubocop-packaging", require: false
|
||||||
gem "rubocop-performance", require: false
|
gem "rubocop-performance", require: false
|
||||||
gem "rubocop-rails", require: false
|
gem "rubocop-rails", require: false
|
||||||
@ -85,7 +89,7 @@ end
|
|||||||
group :storage do
|
group :storage do
|
||||||
gem "aws-sdk-s3", require: false
|
gem "aws-sdk-s3", require: false
|
||||||
gem "google-cloud-storage", "~> 1.11", require: false
|
gem "google-cloud-storage", "~> 1.11", require: false
|
||||||
gem "azure-storage-blob", require: false
|
gem "azure-storage-blob", "~> 2.0", require: false
|
||||||
|
|
||||||
gem "image_processing", "~> 1.2"
|
gem "image_processing", "~> 1.2"
|
||||||
end
|
end
|
||||||
@ -95,7 +99,6 @@ gem "aws-sdk-sns", require: false
|
|||||||
gem "webmock"
|
gem "webmock"
|
||||||
|
|
||||||
group :ujs do
|
group :ujs do
|
||||||
gem "qunit-selenium"
|
|
||||||
gem "webdrivers"
|
gem "webdrivers"
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -111,12 +114,12 @@ instance_eval File.read local_gemfile if File.exist? local_gemfile
|
|||||||
|
|
||||||
group :test do
|
group :test do
|
||||||
gem "minitest-bisect"
|
gem "minitest-bisect"
|
||||||
|
gem "minitest-ci", require: false
|
||||||
gem "minitest-retry"
|
gem "minitest-retry"
|
||||||
gem "minitest-reporters"
|
|
||||||
|
|
||||||
platforms :mri do
|
platforms :mri do
|
||||||
gem "stackprof"
|
gem "stackprof"
|
||||||
gem "byebug"
|
gem "debug", ">= 1.0.0", require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
gem "benchmark-ips"
|
gem "benchmark-ips"
|
||||||
@ -177,6 +180,9 @@ if RUBY_VERSION >= "3.1"
|
|||||||
gem "net-imap", require: false
|
gem "net-imap", require: false
|
||||||
gem "net-pop", require: false
|
gem "net-pop", require: false
|
||||||
|
|
||||||
|
# digest gem, which is one of the default gems has bumped to 3.0.1.pre for ruby 3.1.0dev.
|
||||||
|
gem "digest", "~> 3.0.1.pre", require: false
|
||||||
|
|
||||||
# matrix was removed from default gems in Ruby 3.1, but is used by the `capybara` gem.
|
# matrix was removed from default gems in Ruby 3.1, but is used by the `capybara` gem.
|
||||||
# So we need to add it as a dependency until `capybara` is fixed: https://github.com/teamcapybara/capybara/pull/2468
|
# So we need to add it as a dependency until `capybara` is fixed: https://github.com/teamcapybara/capybara/pull/2468
|
||||||
gem "matrix", require: false
|
gem "matrix", require: false
|
||||||
|
125
Gemfile.lock
125
Gemfile.lock
@ -81,7 +81,6 @@ PATH
|
|||||||
i18n (>= 1.6, < 2)
|
i18n (>= 1.6, < 2)
|
||||||
minitest (>= 5.1)
|
minitest (>= 5.1)
|
||||||
tzinfo (~> 2.0)
|
tzinfo (~> 2.0)
|
||||||
zeitwerk (~> 2.3)
|
|
||||||
rails (7.0.0.alpha)
|
rails (7.0.0.alpha)
|
||||||
actioncable (= 7.0.0.alpha)
|
actioncable (= 7.0.0.alpha)
|
||||||
actionmailbox (= 7.0.0.alpha)
|
actionmailbox (= 7.0.0.alpha)
|
||||||
@ -103,6 +102,7 @@ PATH
|
|||||||
method_source
|
method_source
|
||||||
rake (>= 0.13)
|
rake (>= 0.13)
|
||||||
thor (~> 1.0)
|
thor (~> 1.0)
|
||||||
|
zeitwerk (~> 2.5.0.beta3)
|
||||||
|
|
||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
@ -110,19 +110,18 @@ GEM
|
|||||||
addressable (2.7.0)
|
addressable (2.7.0)
|
||||||
public_suffix (>= 2.0.2, < 5.0)
|
public_suffix (>= 2.0.2, < 5.0)
|
||||||
amq-protocol (2.3.2)
|
amq-protocol (2.3.2)
|
||||||
ansi (1.5.0)
|
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
aws-eventstream (1.1.1)
|
aws-eventstream (1.1.1)
|
||||||
aws-partitions (1.465.0)
|
aws-partitions (1.469.0)
|
||||||
aws-sdk-core (3.114.1)
|
aws-sdk-core (3.114.3)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
aws-partitions (~> 1, >= 1.239.0)
|
aws-partitions (~> 1, >= 1.239.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
jmespath (~> 1.0)
|
jmespath (~> 1.0)
|
||||||
aws-sdk-kms (1.43.0)
|
aws-sdk-kms (1.44.0)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
aws-sdk-s3 (1.96.0)
|
aws-sdk-s3 (1.96.1)
|
||||||
aws-sdk-core (~> 3, >= 3.112.0)
|
aws-sdk-core (~> 3, >= 3.112.0)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.1)
|
aws-sigv4 (~> 1.1)
|
||||||
@ -165,7 +164,6 @@ GEM
|
|||||||
bunny (2.18.0)
|
bunny (2.18.0)
|
||||||
amq-protocol (~> 2.3, >= 2.3.1)
|
amq-protocol (~> 2.3, >= 2.3.1)
|
||||||
sorted_set (~> 1, >= 1.0.2)
|
sorted_set (~> 1, >= 1.0.2)
|
||||||
byebug (11.1.3)
|
|
||||||
capybara (3.35.3)
|
capybara (3.35.3)
|
||||||
addressable
|
addressable
|
||||||
mini_mime (>= 0.1.3)
|
mini_mime (>= 0.1.3)
|
||||||
@ -174,7 +172,7 @@ GEM
|
|||||||
rack-test (>= 0.6.3)
|
rack-test (>= 0.6.3)
|
||||||
regexp_parser (>= 1.5, < 3.0)
|
regexp_parser (>= 1.5, < 3.0)
|
||||||
xpath (~> 3.2)
|
xpath (~> 3.2)
|
||||||
childprocess (4.0.0)
|
childprocess (4.1.0)
|
||||||
coffee-script (2.4.1)
|
coffee-script (2.4.1)
|
||||||
coffee-script-source
|
coffee-script-source
|
||||||
execjs
|
execjs
|
||||||
@ -185,10 +183,15 @@ GEM
|
|||||||
crack (0.4.5)
|
crack (0.4.5)
|
||||||
rexml
|
rexml
|
||||||
crass (1.0.6)
|
crass (1.0.6)
|
||||||
curses (1.4.1)
|
cssbundling-rails (0.1.0)
|
||||||
|
rails (>= 6.0.0)
|
||||||
|
curses (1.4.2)
|
||||||
daemons (1.4.0)
|
daemons (1.4.0)
|
||||||
dalli (2.7.11)
|
dalli (2.7.11)
|
||||||
dante (0.2.0)
|
dante (0.2.0)
|
||||||
|
debug (1.0.0)
|
||||||
|
irb
|
||||||
|
reline (>= 0.2.7)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
delayed_job (4.1.9)
|
delayed_job (4.1.9)
|
||||||
activesupport (>= 3.0, < 6.2)
|
activesupport (>= 3.0, < 6.2)
|
||||||
@ -237,12 +240,12 @@ GEM
|
|||||||
faye-websocket (0.11.1)
|
faye-websocket (0.11.1)
|
||||||
eventmachine (>= 0.12.0)
|
eventmachine (>= 0.12.0)
|
||||||
websocket-driver (>= 0.5.1)
|
websocket-driver (>= 0.5.1)
|
||||||
ffi (1.15.1)
|
ffi (1.15.3)
|
||||||
fugit (1.5.0)
|
fugit (1.5.0)
|
||||||
et-orbi (~> 1.1, >= 1.1.8)
|
et-orbi (~> 1.1, >= 1.1.8)
|
||||||
raabro (~> 1.4)
|
raabro (~> 1.4)
|
||||||
globalid (0.4.2)
|
globalid (0.5.1)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 5.0)
|
||||||
google-apis-core (0.3.0)
|
google-apis-core (0.3.0)
|
||||||
addressable (~> 2.5, >= 2.5.1)
|
addressable (~> 2.5, >= 2.5.1)
|
||||||
googleauth (~> 0.14)
|
googleauth (~> 0.14)
|
||||||
@ -287,14 +290,21 @@ GEM
|
|||||||
image_processing (1.12.1)
|
image_processing (1.12.1)
|
||||||
mini_magick (>= 4.9.5, < 5)
|
mini_magick (>= 4.9.5, < 5)
|
||||||
ruby-vips (>= 2.0.17, < 3)
|
ruby-vips (>= 2.0.17, < 3)
|
||||||
|
importmap-rails (0.5.1)
|
||||||
|
rails (>= 6.0.0)
|
||||||
|
io-console (0.5.9)
|
||||||
|
irb (1.3.7)
|
||||||
|
reline (>= 0.2.7)
|
||||||
jmespath (1.4.0)
|
jmespath (1.4.0)
|
||||||
|
jsbundling-rails (0.1.0)
|
||||||
|
rails (>= 6.0.0)
|
||||||
json (2.5.1)
|
json (2.5.1)
|
||||||
jwt (2.2.3)
|
jwt (2.2.3)
|
||||||
kindlerb (1.2.0)
|
kindlerb (1.2.0)
|
||||||
mustache
|
mustache
|
||||||
nokogiri
|
nokogiri
|
||||||
libxml-ruby (3.2.1)
|
libxml-ruby (3.2.1)
|
||||||
listen (3.5.1)
|
listen (3.6.0)
|
||||||
rb-fsevent (~> 0.10, >= 0.10.3)
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
||||||
rb-inotify (~> 0.9, >= 0.9.10)
|
rb-inotify (~> 0.9, >= 0.9.10)
|
||||||
loofah (2.10.0)
|
loofah (2.10.0)
|
||||||
@ -307,15 +317,13 @@ GEM
|
|||||||
method_source (1.0.0)
|
method_source (1.0.0)
|
||||||
mini_magick (4.11.0)
|
mini_magick (4.11.0)
|
||||||
mini_mime (1.1.0)
|
mini_mime (1.1.0)
|
||||||
|
mini_portile2 (2.5.3)
|
||||||
minitest (5.14.4)
|
minitest (5.14.4)
|
||||||
minitest-bisect (1.5.1)
|
minitest-bisect (1.5.1)
|
||||||
minitest-server (~> 1.0)
|
minitest-server (~> 1.0)
|
||||||
path_expander (~> 1.1)
|
path_expander (~> 1.1)
|
||||||
minitest-reporters (1.4.3)
|
minitest-ci (3.4.0)
|
||||||
ansi
|
minitest (>= 5.0.6)
|
||||||
builder
|
|
||||||
minitest (>= 5.0)
|
|
||||||
ruby-progressbar
|
|
||||||
minitest-retry (0.2.2)
|
minitest-retry (0.2.2)
|
||||||
minitest (>= 5.0)
|
minitest (>= 5.0)
|
||||||
minitest-server (1.0.6)
|
minitest-server (1.0.6)
|
||||||
@ -330,13 +338,16 @@ GEM
|
|||||||
net-http-persistent (4.0.1)
|
net-http-persistent (4.0.1)
|
||||||
connection_pool (~> 2.2)
|
connection_pool (~> 2.2)
|
||||||
nio4r (2.5.7)
|
nio4r (2.5.7)
|
||||||
|
nokogiri (1.11.7)
|
||||||
|
mini_portile2 (~> 2.5.0)
|
||||||
|
racc (~> 1.4)
|
||||||
nokogiri (1.11.7-x86_64-darwin)
|
nokogiri (1.11.7-x86_64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.11.7-x86_64-linux)
|
nokogiri (1.11.7-x86_64-linux)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
os (1.1.1)
|
os (1.1.1)
|
||||||
parallel (1.20.1)
|
parallel (1.20.1)
|
||||||
parser (3.0.1.1)
|
parser (3.0.2.0)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
path_expander (1.1.0)
|
path_expander (1.1.0)
|
||||||
pg (1.2.3)
|
pg (1.2.3)
|
||||||
@ -345,9 +356,6 @@ GEM
|
|||||||
puma (5.3.2)
|
puma (5.3.2)
|
||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
que (0.14.3)
|
que (0.14.3)
|
||||||
qunit-selenium (0.0.4)
|
|
||||||
selenium-webdriver
|
|
||||||
thor
|
|
||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.5.2)
|
racc (1.5.2)
|
||||||
rack (2.2.3)
|
rack (2.2.3)
|
||||||
@ -355,8 +363,6 @@ GEM
|
|||||||
rack (>= 0.4)
|
rack (>= 0.4)
|
||||||
rack-protection (2.1.0)
|
rack-protection (2.1.0)
|
||||||
rack
|
rack
|
||||||
rack-proxy (0.7.0)
|
|
||||||
rack
|
|
||||||
rack-test (1.1.0)
|
rack-test (1.1.0)
|
||||||
rack (>= 1.0, < 3)
|
rack (>= 1.0, < 3)
|
||||||
rails-dom-testing (2.0.3)
|
rails-dom-testing (2.0.3)
|
||||||
@ -372,10 +378,12 @@ GEM
|
|||||||
rbtree (0.4.4)
|
rbtree (0.4.4)
|
||||||
rdoc (6.3.1)
|
rdoc (6.3.1)
|
||||||
redcarpet (3.2.3)
|
redcarpet (3.2.3)
|
||||||
redis (4.2.5)
|
redis (4.3.1)
|
||||||
redis-namespace (1.8.1)
|
redis-namespace (1.8.1)
|
||||||
redis (>= 3.0.4)
|
redis (>= 3.0.4)
|
||||||
regexp_parser (2.1.1)
|
regexp_parser (2.1.1)
|
||||||
|
reline (0.2.7)
|
||||||
|
io-console (~> 0.5)
|
||||||
representable (3.1.1)
|
representable (3.1.1)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
@ -394,23 +402,25 @@ GEM
|
|||||||
retriable (3.1.2)
|
retriable (3.1.2)
|
||||||
rexml (3.2.5)
|
rexml (3.2.5)
|
||||||
rouge (3.26.0)
|
rouge (3.26.0)
|
||||||
rubocop (1.16.0)
|
rubocop (1.19.0)
|
||||||
parallel (~> 1.10)
|
parallel (~> 1.10)
|
||||||
parser (>= 3.0.0.0)
|
parser (>= 3.0.0.0)
|
||||||
rainbow (>= 2.2.2, < 4.0)
|
rainbow (>= 2.2.2, < 4.0)
|
||||||
regexp_parser (>= 1.8, < 3.0)
|
regexp_parser (>= 1.8, < 3.0)
|
||||||
rexml
|
rexml
|
||||||
rubocop-ast (>= 1.7.0, < 2.0)
|
rubocop-ast (>= 1.9.1, < 2.0)
|
||||||
ruby-progressbar (~> 1.7)
|
ruby-progressbar (~> 1.7)
|
||||||
unicode-display_width (>= 1.4.0, < 3.0)
|
unicode-display_width (>= 1.4.0, < 3.0)
|
||||||
rubocop-ast (1.7.0)
|
rubocop-ast (1.9.1)
|
||||||
parser (>= 3.0.1.1)
|
parser (>= 3.0.1.1)
|
||||||
|
rubocop-minitest (0.15.0)
|
||||||
|
rubocop (>= 0.90, < 2.0)
|
||||||
rubocop-packaging (0.5.1)
|
rubocop-packaging (0.5.1)
|
||||||
rubocop (>= 0.89, < 2.0)
|
rubocop (>= 0.89, < 2.0)
|
||||||
rubocop-performance (1.11.3)
|
rubocop-performance (1.11.4)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
rubocop-ast (>= 0.4.0)
|
rubocop-ast (>= 0.4.0)
|
||||||
rubocop-rails (2.10.1)
|
rubocop-rails (2.11.3)
|
||||||
activesupport (>= 4.2.0)
|
activesupport (>= 4.2.0)
|
||||||
rack (>= 1.1)
|
rack (>= 1.1)
|
||||||
rubocop (>= 1.7.0, < 2.0)
|
rubocop (>= 1.7.0, < 2.0)
|
||||||
@ -421,23 +431,12 @@ GEM
|
|||||||
rubyzip (2.3.0)
|
rubyzip (2.3.0)
|
||||||
rufus-scheduler (3.7.0)
|
rufus-scheduler (3.7.0)
|
||||||
fugit (~> 1.1, >= 1.1.6)
|
fugit (~> 1.1, >= 1.1.6)
|
||||||
sass-rails (6.0.0)
|
|
||||||
sassc-rails (~> 2.1, >= 2.1.1)
|
|
||||||
sassc (2.4.0)
|
|
||||||
ffi (~> 1.9)
|
|
||||||
sassc-rails (2.1.2)
|
|
||||||
railties (>= 4.0.0)
|
|
||||||
sassc (>= 2.0)
|
|
||||||
sprockets (> 3.0)
|
|
||||||
sprockets-rails
|
|
||||||
tilt
|
|
||||||
sdoc (2.2.0)
|
sdoc (2.2.0)
|
||||||
rdoc (>= 5.0)
|
rdoc (>= 5.0)
|
||||||
selenium-webdriver (4.0.0.beta4)
|
selenium-webdriver (4.0.0.beta4)
|
||||||
childprocess (>= 0.5, < 5.0)
|
childprocess (>= 0.5, < 5.0)
|
||||||
rexml (~> 3.2)
|
rexml (~> 3.2)
|
||||||
rubyzip (>= 1.2.2)
|
rubyzip (>= 1.2.2)
|
||||||
semantic_range (3.0.0)
|
|
||||||
sequel (5.45.0)
|
sequel (5.45.0)
|
||||||
serverengine (2.0.7)
|
serverengine (2.0.7)
|
||||||
sigdump (~> 0.2.2)
|
sigdump (~> 0.2.2)
|
||||||
@ -476,8 +475,14 @@ GEM
|
|||||||
sprockets (>= 3.0.0)
|
sprockets (>= 3.0.0)
|
||||||
sqlite3 (1.4.2)
|
sqlite3 (1.4.2)
|
||||||
stackprof (0.2.17)
|
stackprof (0.2.17)
|
||||||
|
stimulus-rails (0.4.2)
|
||||||
|
rails (>= 6.0.0)
|
||||||
sucker_punch (3.0.1)
|
sucker_punch (3.0.1)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
|
tailwindcss-rails (0.4.3)
|
||||||
|
rails (>= 6.0.0)
|
||||||
|
terser (1.1.4)
|
||||||
|
execjs (>= 0.3.0, < 3)
|
||||||
thin (1.8.1)
|
thin (1.8.1)
|
||||||
daemons (~> 1.0, >= 1.0.9)
|
daemons (~> 1.0, >= 1.0.9)
|
||||||
eventmachine (~> 1.0, >= 1.0.4)
|
eventmachine (~> 1.0, >= 1.0.4)
|
||||||
@ -485,14 +490,11 @@ GEM
|
|||||||
thor (1.1.0)
|
thor (1.1.0)
|
||||||
tilt (2.0.10)
|
tilt (2.0.10)
|
||||||
trailblazer-option (0.1.1)
|
trailblazer-option (0.1.1)
|
||||||
turbolinks (5.2.1)
|
turbo-rails (0.7.11)
|
||||||
turbolinks-source (~> 5.2)
|
rails (>= 6.0.0)
|
||||||
turbolinks-source (5.2.0)
|
|
||||||
tzinfo (2.0.4)
|
tzinfo (2.0.4)
|
||||||
concurrent-ruby (~> 1.0)
|
concurrent-ruby (~> 1.0)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
uglifier (4.2.0)
|
|
||||||
execjs (>= 0.3.0, < 3)
|
|
||||||
unicode-display_width (2.0.0)
|
unicode-display_width (2.0.0)
|
||||||
useragent (0.16.10)
|
useragent (0.16.10)
|
||||||
vegas (0.1.11)
|
vegas (0.1.11)
|
||||||
@ -509,21 +511,17 @@ GEM
|
|||||||
addressable (>= 2.3.6)
|
addressable (>= 2.3.6)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
hashdiff (>= 0.4.0, < 2.0.0)
|
||||||
webpacker (5.4.0)
|
|
||||||
activesupport (>= 5.2)
|
|
||||||
rack-proxy (>= 0.6.1)
|
|
||||||
railties (>= 5.2)
|
|
||||||
semantic_range (>= 2.3.0)
|
|
||||||
webrick (1.7.0)
|
webrick (1.7.0)
|
||||||
websocket (1.2.9)
|
websocket (1.2.9)
|
||||||
websocket-driver (0.7.4)
|
websocket-driver (0.7.5)
|
||||||
websocket-extensions (>= 0.1.0)
|
websocket-extensions (>= 0.1.0)
|
||||||
websocket-extensions (0.1.5)
|
websocket-extensions (0.1.5)
|
||||||
xpath (3.2.0)
|
xpath (3.2.0)
|
||||||
nokogiri (~> 1.8)
|
nokogiri (~> 1.8)
|
||||||
zeitwerk (2.4.2)
|
zeitwerk (2.5.0.beta3)
|
||||||
|
|
||||||
PLATFORMS
|
PLATFORMS
|
||||||
|
ruby
|
||||||
x86_64-darwin-19
|
x86_64-darwin-19
|
||||||
x86_64-darwin-20
|
x86_64-darwin-20
|
||||||
x86_64-linux
|
x86_64-linux
|
||||||
@ -534,27 +532,30 @@ DEPENDENCIES
|
|||||||
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
|
activerecord-jdbcsqlite3-adapter (>= 1.3.0)
|
||||||
aws-sdk-s3
|
aws-sdk-s3
|
||||||
aws-sdk-sns
|
aws-sdk-sns
|
||||||
azure-storage-blob
|
azure-storage-blob (~> 2.0)
|
||||||
backburner
|
backburner
|
||||||
bcrypt (~> 3.1.11)
|
bcrypt (~> 3.1.11)
|
||||||
benchmark-ips
|
benchmark-ips
|
||||||
blade
|
blade
|
||||||
bootsnap (>= 1.4.4)
|
bootsnap (>= 1.4.4)
|
||||||
byebug
|
|
||||||
capybara (>= 3.26)
|
capybara (>= 3.26)
|
||||||
connection_pool
|
connection_pool
|
||||||
|
cssbundling-rails
|
||||||
dalli
|
dalli
|
||||||
|
debug (>= 1.0.0)
|
||||||
delayed_job
|
delayed_job
|
||||||
delayed_job_active_record
|
delayed_job_active_record
|
||||||
google-cloud-storage (~> 1.11)
|
google-cloud-storage (~> 1.11)
|
||||||
hiredis
|
hiredis
|
||||||
image_processing (~> 1.2)
|
image_processing (~> 1.2)
|
||||||
|
importmap-rails
|
||||||
|
jsbundling-rails
|
||||||
json (>= 2.0.0)
|
json (>= 2.0.0)
|
||||||
kindlerb (~> 1.2.0)
|
kindlerb (~> 1.2.0)
|
||||||
libxml-ruby
|
libxml-ruby
|
||||||
listen (~> 3.3)
|
listen (~> 3.3)
|
||||||
minitest-bisect
|
minitest-bisect
|
||||||
minitest-reporters
|
minitest-ci
|
||||||
minitest-retry
|
minitest-retry
|
||||||
mysql2 (~> 0.5)!
|
mysql2 (~> 0.5)!
|
||||||
nokogiri (>= 1.8.1, != 1.11.0)
|
nokogiri (>= 1.8.1, != 1.11.0)
|
||||||
@ -563,7 +564,6 @@ DEPENDENCIES
|
|||||||
puma
|
puma
|
||||||
que
|
que
|
||||||
queue_classic!
|
queue_classic!
|
||||||
qunit-selenium
|
|
||||||
racc (>= 1.4.6)
|
racc (>= 1.4.6)
|
||||||
rack-cache (~> 1.2)
|
rack-cache (~> 1.2)
|
||||||
rails!
|
rails!
|
||||||
@ -576,10 +576,10 @@ DEPENDENCIES
|
|||||||
rexml
|
rexml
|
||||||
rouge
|
rouge
|
||||||
rubocop (>= 0.90)
|
rubocop (>= 0.90)
|
||||||
|
rubocop-minitest
|
||||||
rubocop-packaging
|
rubocop-packaging
|
||||||
rubocop-performance
|
rubocop-performance
|
||||||
rubocop-rails
|
rubocop-rails
|
||||||
sass-rails
|
|
||||||
sdoc (>= 2.2.0)
|
sdoc (>= 2.2.0)
|
||||||
selenium-webdriver (>= 4.0.0.alpha7)
|
selenium-webdriver (>= 4.0.0.alpha7)
|
||||||
sequel
|
sequel
|
||||||
@ -588,17 +588,18 @@ DEPENDENCIES
|
|||||||
sprockets-export
|
sprockets-export
|
||||||
sqlite3 (~> 1.4)
|
sqlite3 (~> 1.4)
|
||||||
stackprof
|
stackprof
|
||||||
|
stimulus-rails
|
||||||
sucker_punch
|
sucker_punch
|
||||||
turbolinks (~> 5)
|
tailwindcss-rails
|
||||||
|
terser (>= 1.1.4)
|
||||||
|
turbo-rails
|
||||||
tzinfo-data
|
tzinfo-data
|
||||||
uglifier (>= 1.3.0)
|
|
||||||
w3c_validators (~> 1.3.6)
|
w3c_validators (~> 1.3.6)
|
||||||
wdm (>= 0.1.0)
|
wdm (>= 0.1.0)
|
||||||
webdrivers
|
webdrivers
|
||||||
webmock
|
webmock
|
||||||
webpacker (~> 5.0)
|
|
||||||
webrick
|
webrick
|
||||||
websocket-client-simple!
|
websocket-client-simple!
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.2.19
|
2.2.25
|
||||||
|
@ -49,12 +49,12 @@ give them a heads up that Rails will be released soonish.
|
|||||||
This is only required for major and minor releases, bugfix releases aren't a
|
This is only required for major and minor releases, bugfix releases aren't a
|
||||||
big enough deal, and are supposed to be backward compatible.
|
big enough deal, and are supposed to be backward compatible.
|
||||||
|
|
||||||
Send an email just giving a heads up about the upcoming release to these
|
Send a message just giving a heads up about the upcoming release to these
|
||||||
lists:
|
lists:
|
||||||
|
|
||||||
* team@jruby.org
|
* team@jruby.org
|
||||||
* community@rubini.us
|
* community@rubini.us
|
||||||
* rubyonrails-core@googlegroups.com
|
* [rubyonrails-core](https://discuss.rubyonrails.org/c/rubyonrails-core)
|
||||||
|
|
||||||
Implementors will love you and help you.
|
Implementors will love you and help you.
|
||||||
|
|
||||||
@ -135,8 +135,8 @@ Write a release announcement that includes the version, changes, and links to
|
|||||||
GitHub where people can find the specific commit list. Here are the mailing
|
GitHub where people can find the specific commit list. Here are the mailing
|
||||||
lists where you should announce:
|
lists where you should announce:
|
||||||
|
|
||||||
* rubyonrails-core@googlegroups.com
|
* [rubyonrails-core](https://discuss.rubyonrails.org/c/rubyonrails-core)
|
||||||
* rubyonrails-talk@googlegroups.com
|
* [rubyonrails-talk](https://discuss.rubyonrails.org/c/rubyonrails-talk)
|
||||||
* ruby-talk@ruby-lang.org
|
* ruby-talk@ruby-lang.org
|
||||||
|
|
||||||
Use Markdown format for your announcement. Remember to ask people to report
|
Use Markdown format for your announcement. Remember to ask people to report
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
"rules": {
|
"rules": {
|
||||||
"semi": ["error", "never"],
|
"semi": ["error", "never"],
|
||||||
"quotes": ["error", "double"],
|
"quotes": ["error", "double"],
|
||||||
"no-unused-vars": ["error", { "vars": "all", "args": "none" }]
|
"no-unused-vars": ["error", { "vars": "all", "args": "none" }],
|
||||||
|
"no-console": "off"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"import"
|
"import"
|
||||||
|
@ -1,3 +1,23 @@
|
|||||||
|
* Compile ESM package that can be used directly in the browser as actioncable.esm.js.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
|
* Move action_cable.js to actioncable.js to match naming convention used for other Rails frameworks, and use JS console to communicate the deprecation.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
|
* Stop transpiling the UMD package generated as actioncable.js and drop the IE11 testing that relied on that.
|
||||||
|
|
||||||
|
*DHH*
|
||||||
|
|
||||||
|
* Truncate broadcast logging messages.
|
||||||
|
|
||||||
|
*J Smith*
|
||||||
|
|
||||||
|
* OpenSSL constants are now used for Digest computations.
|
||||||
|
|
||||||
|
*Dirkjan Bussink*
|
||||||
|
|
||||||
* The Action Cable client now includes safeguards to prevent a "thundering
|
* The Action Cable client now includes safeguards to prevent a "thundering
|
||||||
herd" of client reconnects after server connectivity loss:
|
herd" of client reconnects after server connectivity loss:
|
||||||
|
|
||||||
@ -11,4 +31,5 @@
|
|||||||
|
|
||||||
*Jonathan Hefner*
|
*Jonathan Hefner*
|
||||||
|
|
||||||
|
|
||||||
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actioncable/CHANGELOG.md) for previous changes.
|
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actioncable/CHANGELOG.md) for previous changes.
|
||||||
|
433
actioncable/app/assets/javascripts/action_cable.js
generated
433
actioncable/app/assets/javascripts/action_cable.js
generated
@ -1,153 +1,113 @@
|
|||||||
(function(global, factory) {
|
(function(global, factory) {
|
||||||
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {});
|
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
})(this, function(exports) {
|
factory(global.ActionCable = {}));
|
||||||
|
})(this, (function(exports) {
|
||||||
"use strict";
|
"use strict";
|
||||||
var adapters = {
|
var adapters = {
|
||||||
logger: self.console,
|
logger: self.console,
|
||||||
WebSocket: self.WebSocket
|
WebSocket: self.WebSocket
|
||||||
};
|
};
|
||||||
var logger = {
|
var logger = {
|
||||||
log: function log() {
|
log(...messages) {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
var _adapters$logger;
|
|
||||||
for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
|
|
||||||
messages[_key] = arguments[_key];
|
|
||||||
}
|
|
||||||
messages.push(Date.now());
|
messages.push(Date.now());
|
||||||
(_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages));
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
|
const now = () => (new Date).getTime();
|
||||||
return typeof obj;
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
} : function(obj) {
|
class ConnectionMonitor {
|
||||||
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
|
constructor(connection) {
|
||||||
};
|
|
||||||
var classCallCheck = function(instance, Constructor) {
|
|
||||||
if (!(instance instanceof Constructor)) {
|
|
||||||
throw new TypeError("Cannot call a class as a function");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
var createClass = function() {
|
|
||||||
function defineProperties(target, props) {
|
|
||||||
for (var i = 0; i < props.length; i++) {
|
|
||||||
var descriptor = props[i];
|
|
||||||
descriptor.enumerable = descriptor.enumerable || false;
|
|
||||||
descriptor.configurable = true;
|
|
||||||
if ("value" in descriptor) descriptor.writable = true;
|
|
||||||
Object.defineProperty(target, descriptor.key, descriptor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return function(Constructor, protoProps, staticProps) {
|
|
||||||
if (protoProps) defineProperties(Constructor.prototype, protoProps);
|
|
||||||
if (staticProps) defineProperties(Constructor, staticProps);
|
|
||||||
return Constructor;
|
|
||||||
};
|
|
||||||
}();
|
|
||||||
var now = function now() {
|
|
||||||
return new Date().getTime();
|
|
||||||
};
|
|
||||||
var secondsSince = function secondsSince(time) {
|
|
||||||
return (now() - time) / 1e3;
|
|
||||||
};
|
|
||||||
var ConnectionMonitor = function() {
|
|
||||||
function ConnectionMonitor(connection) {
|
|
||||||
classCallCheck(this, ConnectionMonitor);
|
|
||||||
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
this.connection = connection;
|
this.connection = connection;
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
}
|
}
|
||||||
ConnectionMonitor.prototype.start = function start() {
|
start() {
|
||||||
if (!this.isRunning()) {
|
if (!this.isRunning()) {
|
||||||
this.startedAt = now();
|
this.startedAt = now();
|
||||||
delete this.stoppedAt;
|
delete this.stoppedAt;
|
||||||
this.startPolling();
|
this.startPolling();
|
||||||
addEventListener("visibilitychange", this.visibilityDidChange);
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
logger.log("ConnectionMonitor started. stale threshold = " + this.constructor.staleThreshold + " s");
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.stop = function stop() {
|
stop() {
|
||||||
if (this.isRunning()) {
|
if (this.isRunning()) {
|
||||||
this.stoppedAt = now();
|
this.stoppedAt = now();
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
removeEventListener("visibilitychange", this.visibilityDidChange);
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
logger.log("ConnectionMonitor stopped");
|
logger.log("ConnectionMonitor stopped");
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.isRunning = function isRunning() {
|
isRunning() {
|
||||||
return this.startedAt && !this.stoppedAt;
|
return this.startedAt && !this.stoppedAt;
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordPing = function recordPing() {
|
recordPing() {
|
||||||
this.pingedAt = now();
|
this.pingedAt = now();
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordConnect = function recordConnect() {
|
recordConnect() {
|
||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.recordPing();
|
this.recordPing();
|
||||||
delete this.disconnectedAt;
|
delete this.disconnectedAt;
|
||||||
logger.log("ConnectionMonitor recorded connect");
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
|
recordDisconnect() {
|
||||||
this.disconnectedAt = now();
|
this.disconnectedAt = now();
|
||||||
logger.log("ConnectionMonitor recorded disconnect");
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.startPolling = function startPolling() {
|
startPolling() {
|
||||||
this.stopPolling();
|
this.stopPolling();
|
||||||
this.poll();
|
this.poll();
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.stopPolling = function stopPolling() {
|
stopPolling() {
|
||||||
clearTimeout(this.pollTimeout);
|
clearTimeout(this.pollTimeout);
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.poll = function poll() {
|
poll() {
|
||||||
var _this = this;
|
this.pollTimeout = setTimeout((() => {
|
||||||
this.pollTimeout = setTimeout(function() {
|
this.reconnectIfStale();
|
||||||
_this.reconnectIfStale();
|
this.poll();
|
||||||
_this.poll();
|
}), this.getPollInterval());
|
||||||
}, this.getPollInterval());
|
}
|
||||||
};
|
getPollInterval() {
|
||||||
ConnectionMonitor.prototype.getPollInterval = function getPollInterval() {
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
var _constructor = this.constructor, staleThreshold = _constructor.staleThreshold, reconnectionBackoffRate = _constructor.reconnectionBackoffRate;
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
var backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
var jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
const jitter = jitterMax * Math.random();
|
||||||
var jitter = jitterMax * Math.random();
|
|
||||||
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
};
|
}
|
||||||
ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
|
reconnectIfStale() {
|
||||||
if (this.connectionIsStale()) {
|
if (this.connectionIsStale()) {
|
||||||
logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", time stale = " + secondsSince(this.refreshedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
this.reconnectAttempts++;
|
this.reconnectAttempts++;
|
||||||
if (this.disconnectedRecently()) {
|
if (this.disconnectedRecently()) {
|
||||||
logger.log("ConnectionMonitor skipping reopening recent disconnect. time disconnected = " + secondsSince(this.disconnectedAt) + " s");
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
} else {
|
} else {
|
||||||
logger.log("ConnectionMonitor reopening");
|
logger.log("ConnectionMonitor reopening");
|
||||||
this.connection.reopen();
|
this.connection.reopen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() {
|
|
||||||
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
|
||||||
};
|
|
||||||
ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() {
|
|
||||||
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
|
||||||
};
|
|
||||||
ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() {
|
|
||||||
var _this2 = this;
|
|
||||||
if (document.visibilityState === "visible") {
|
|
||||||
setTimeout(function() {
|
|
||||||
if (_this2.connectionIsStale() || !_this2.connection.isOpen()) {
|
|
||||||
logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = " + document.visibilityState);
|
|
||||||
_this2.connection.reopen();
|
|
||||||
}
|
}
|
||||||
}, 200);
|
get refreshedAt() {
|
||||||
}
|
|
||||||
};
|
|
||||||
createClass(ConnectionMonitor, [ {
|
|
||||||
key: "refreshedAt",
|
|
||||||
get: function get$$1() {
|
|
||||||
return this.pingedAt ? this.pingedAt : this.startedAt;
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
}
|
}
|
||||||
} ]);
|
connectionIsStale() {
|
||||||
return ConnectionMonitor;
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
}();
|
}
|
||||||
|
disconnectedRecently() {
|
||||||
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
visibilityDidChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
setTimeout((() => {
|
||||||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
ConnectionMonitor.staleThreshold = 6;
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
ConnectionMonitor.reconnectionBackoffRate = .15;
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
var INTERNAL = {
|
var INTERNAL = {
|
||||||
@ -166,32 +126,31 @@
|
|||||||
default_mount_path: "/cable",
|
default_mount_path: "/cable",
|
||||||
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
||||||
};
|
};
|
||||||
var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols;
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
var supportedProtocols = protocols.slice(0, protocols.length - 1);
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
var indexOf = [].indexOf;
|
const indexOf = [].indexOf;
|
||||||
var Connection = function() {
|
class Connection {
|
||||||
function Connection(consumer) {
|
constructor(consumer) {
|
||||||
classCallCheck(this, Connection);
|
|
||||||
this.open = this.open.bind(this);
|
this.open = this.open.bind(this);
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.subscriptions = this.consumer.subscriptions;
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
this.monitor = new ConnectionMonitor(this);
|
this.monitor = new ConnectionMonitor(this);
|
||||||
this.disconnected = true;
|
this.disconnected = true;
|
||||||
}
|
}
|
||||||
Connection.prototype.send = function send(data) {
|
send(data) {
|
||||||
if (this.isOpen()) {
|
if (this.isOpen()) {
|
||||||
this.webSocket.send(JSON.stringify(data));
|
this.webSocket.send(JSON.stringify(data));
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.open = function open() {
|
open() {
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
logger.log("Attempted to open WebSocket, but existing socket is " + this.getState());
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
|
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
this.uninstallEventHandlers();
|
this.uninstallEventHandlers();
|
||||||
}
|
}
|
||||||
@ -200,90 +159,85 @@
|
|||||||
this.monitor.start();
|
this.monitor.start();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.close = function close() {
|
close({allowReconnect: allowReconnect} = {
|
||||||
var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
|
|
||||||
allowReconnect: true
|
allowReconnect: true
|
||||||
}, allowReconnect = _ref.allowReconnect;
|
}) {
|
||||||
if (!allowReconnect) {
|
if (!allowReconnect) {
|
||||||
this.monitor.stop();
|
this.monitor.stop();
|
||||||
}
|
}
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
return this.webSocket.close();
|
return this.webSocket.close();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.reopen = function reopen() {
|
reopen() {
|
||||||
logger.log("Reopening WebSocket, current state is " + this.getState());
|
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
||||||
if (this.isActive()) {
|
if (this.isActive()) {
|
||||||
try {
|
try {
|
||||||
return this.close();
|
return this.close();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log("Failed to reopen WebSocket", error);
|
logger.log("Failed to reopen WebSocket", error);
|
||||||
} finally {
|
} finally {
|
||||||
logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
|
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
||||||
setTimeout(this.open, this.constructor.reopenDelay);
|
setTimeout(this.open, this.constructor.reopenDelay);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return this.open();
|
return this.open();
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
Connection.prototype.getProtocol = function getProtocol() {
|
getProtocol() {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
return this.webSocket.protocol;
|
return this.webSocket.protocol;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
Connection.prototype.isOpen = function isOpen() {
|
|
||||||
return this.isState("open");
|
|
||||||
};
|
|
||||||
Connection.prototype.isActive = function isActive() {
|
|
||||||
return this.isState("open", "connecting");
|
|
||||||
};
|
|
||||||
Connection.prototype.isProtocolSupported = function isProtocolSupported() {
|
|
||||||
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
|
||||||
};
|
|
||||||
Connection.prototype.isState = function isState() {
|
|
||||||
for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) {
|
|
||||||
states[_key] = arguments[_key];
|
|
||||||
}
|
}
|
||||||
|
isOpen() {
|
||||||
|
return this.isState("open");
|
||||||
|
}
|
||||||
|
isActive() {
|
||||||
|
return this.isState("open", "connecting");
|
||||||
|
}
|
||||||
|
isProtocolSupported() {
|
||||||
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
|
}
|
||||||
|
isState(...states) {
|
||||||
return indexOf.call(states, this.getState()) >= 0;
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
};
|
}
|
||||||
Connection.prototype.getState = function getState() {
|
getState() {
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
for (var state in adapters.WebSocket) {
|
for (let state in adapters.WebSocket) {
|
||||||
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
return state.toLowerCase();
|
return state.toLowerCase();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
};
|
|
||||||
Connection.prototype.installEventHandlers = function installEventHandlers() {
|
|
||||||
for (var eventName in this.events) {
|
|
||||||
var handler = this.events[eventName].bind(this);
|
|
||||||
this.webSocket["on" + eventName] = handler;
|
|
||||||
}
|
}
|
||||||
};
|
installEventHandlers() {
|
||||||
Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() {
|
for (let eventName in this.events) {
|
||||||
for (var eventName in this.events) {
|
const handler = this.events[eventName].bind(this);
|
||||||
this.webSocket["on" + eventName] = function() {};
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
return Connection;
|
|
||||||
}();
|
|
||||||
Connection.reopenDelay = 500;
|
Connection.reopenDelay = 500;
|
||||||
Connection.prototype.events = {
|
Connection.prototype.events = {
|
||||||
message: function message(event) {
|
message(event) {
|
||||||
if (!this.isProtocolSupported()) {
|
if (!this.isProtocolSupported()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type;
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case message_types.welcome:
|
case message_types.welcome:
|
||||||
this.monitor.recordConnect();
|
this.monitor.recordConnect();
|
||||||
return this.subscriptions.reload();
|
return this.subscriptions.reload();
|
||||||
|
|
||||||
case message_types.disconnect:
|
case message_types.disconnect:
|
||||||
logger.log("Disconnecting. Reason: " + reason);
|
logger.log(`Disconnecting. Reason: ${reason}`);
|
||||||
return this.close({
|
return this.close({
|
||||||
allowReconnect: reconnect
|
allowReconnect: reconnect
|
||||||
});
|
});
|
||||||
@ -301,8 +255,8 @@
|
|||||||
return this.subscriptions.notify(identifier, "received", message);
|
return this.subscriptions.notify(identifier, "received", message);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
open: function open() {
|
open() {
|
||||||
logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
|
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
||||||
this.disconnected = false;
|
this.disconnected = false;
|
||||||
if (!this.isProtocolSupported()) {
|
if (!this.isProtocolSupported()) {
|
||||||
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
||||||
@ -311,7 +265,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
close: function close(event) {
|
close(event) {
|
||||||
logger.log("WebSocket onclose event");
|
logger.log("WebSocket onclose event");
|
||||||
if (this.disconnected) {
|
if (this.disconnected) {
|
||||||
return;
|
return;
|
||||||
@ -322,167 +276,136 @@
|
|||||||
willAttemptReconnect: this.monitor.isRunning()
|
willAttemptReconnect: this.monitor.isRunning()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
error: function error() {
|
error() {
|
||||||
logger.log("WebSocket onerror event");
|
logger.log("WebSocket onerror event");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var extend = function extend(object, properties) {
|
const extend = function(object, properties) {
|
||||||
if (properties != null) {
|
if (properties != null) {
|
||||||
for (var key in properties) {
|
for (let key in properties) {
|
||||||
var value = properties[key];
|
const value = properties[key];
|
||||||
object[key] = value;
|
object[key] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return object;
|
return object;
|
||||||
};
|
};
|
||||||
var Subscription = function() {
|
class Subscription {
|
||||||
function Subscription(consumer) {
|
constructor(consumer, params = {}, mixin) {
|
||||||
var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
||||||
var mixin = arguments[2];
|
|
||||||
classCallCheck(this, Subscription);
|
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.identifier = JSON.stringify(params);
|
this.identifier = JSON.stringify(params);
|
||||||
extend(this, mixin);
|
extend(this, mixin);
|
||||||
}
|
}
|
||||||
Subscription.prototype.perform = function perform(action) {
|
perform(action, data = {}) {
|
||||||
var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
||||||
data.action = action;
|
data.action = action;
|
||||||
return this.send(data);
|
return this.send(data);
|
||||||
};
|
}
|
||||||
Subscription.prototype.send = function send(data) {
|
send(data) {
|
||||||
return this.consumer.send({
|
return this.consumer.send({
|
||||||
command: "message",
|
command: "message",
|
||||||
identifier: this.identifier,
|
identifier: this.identifier,
|
||||||
data: JSON.stringify(data)
|
data: JSON.stringify(data)
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
Subscription.prototype.unsubscribe = function unsubscribe() {
|
unsubscribe() {
|
||||||
return this.consumer.subscriptions.remove(this);
|
return this.consumer.subscriptions.remove(this);
|
||||||
};
|
}
|
||||||
return Subscription;
|
}
|
||||||
}();
|
class Subscriptions {
|
||||||
var Subscriptions = function() {
|
constructor(consumer) {
|
||||||
function Subscriptions(consumer) {
|
|
||||||
classCallCheck(this, Subscriptions);
|
|
||||||
this.consumer = consumer;
|
this.consumer = consumer;
|
||||||
this.subscriptions = [];
|
this.subscriptions = [];
|
||||||
}
|
}
|
||||||
Subscriptions.prototype.create = function create(channelName, mixin) {
|
create(channelName, mixin) {
|
||||||
var channel = channelName;
|
const channel = channelName;
|
||||||
var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : {
|
const params = typeof channel === "object" ? channel : {
|
||||||
channel: channel
|
channel: channel
|
||||||
};
|
};
|
||||||
var subscription = new Subscription(this.consumer, params, mixin);
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
return this.add(subscription);
|
return this.add(subscription);
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.add = function add(subscription) {
|
add(subscription) {
|
||||||
this.subscriptions.push(subscription);
|
this.subscriptions.push(subscription);
|
||||||
this.consumer.ensureActiveConnection();
|
this.consumer.ensureActiveConnection();
|
||||||
this.notify(subscription, "initialized");
|
this.notify(subscription, "initialized");
|
||||||
this.sendCommand(subscription, "subscribe");
|
this.sendCommand(subscription, "subscribe");
|
||||||
return subscription;
|
return subscription;
|
||||||
};
|
}
|
||||||
Subscriptions.prototype.remove = function remove(subscription) {
|
remove(subscription) {
|
||||||
this.forget(subscription);
|
this.forget(subscription);
|
||||||
if (!this.findAll(subscription.identifier).length) {
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
this.sendCommand(subscription, "unsubscribe");
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
}
|
}
|
||||||
return subscription;
|
return subscription;
|
||||||
};
|
|
||||||
Subscriptions.prototype.reject = function reject(identifier) {
|
|
||||||
var _this = this;
|
|
||||||
return this.findAll(identifier).map(function(subscription) {
|
|
||||||
_this.forget(subscription);
|
|
||||||
_this.notify(subscription, "rejected");
|
|
||||||
return subscription;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Subscriptions.prototype.forget = function forget(subscription) {
|
|
||||||
this.subscriptions = this.subscriptions.filter(function(s) {
|
|
||||||
return s !== subscription;
|
|
||||||
});
|
|
||||||
return subscription;
|
|
||||||
};
|
|
||||||
Subscriptions.prototype.findAll = function findAll(identifier) {
|
|
||||||
return this.subscriptions.filter(function(s) {
|
|
||||||
return s.identifier === identifier;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Subscriptions.prototype.reload = function reload() {
|
|
||||||
var _this2 = this;
|
|
||||||
return this.subscriptions.map(function(subscription) {
|
|
||||||
return _this2.sendCommand(subscription, "subscribe");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
Subscriptions.prototype.notifyAll = function notifyAll(callbackName) {
|
|
||||||
var _this3 = this;
|
|
||||||
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
|
|
||||||
args[_key - 1] = arguments[_key];
|
|
||||||
}
|
}
|
||||||
return this.subscriptions.map(function(subscription) {
|
reject(identifier) {
|
||||||
return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args));
|
return this.findAll(identifier).map((subscription => {
|
||||||
});
|
this.forget(subscription);
|
||||||
};
|
this.notify(subscription, "rejected");
|
||||||
Subscriptions.prototype.notify = function notify(subscription, callbackName) {
|
return subscription;
|
||||||
for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
|
}));
|
||||||
args[_key2 - 2] = arguments[_key2];
|
|
||||||
}
|
}
|
||||||
var subscriptions = void 0;
|
forget(subscription) {
|
||||||
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
findAll(identifier) {
|
||||||
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
|
}
|
||||||
|
reload() {
|
||||||
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
|
}
|
||||||
|
notifyAll(callbackName, ...args) {
|
||||||
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
|
}
|
||||||
|
notify(subscription, callbackName, ...args) {
|
||||||
|
let subscriptions;
|
||||||
if (typeof subscription === "string") {
|
if (typeof subscription === "string") {
|
||||||
subscriptions = this.findAll(subscription);
|
subscriptions = this.findAll(subscription);
|
||||||
} else {
|
} else {
|
||||||
subscriptions = [ subscription ];
|
subscriptions = [ subscription ];
|
||||||
}
|
}
|
||||||
return subscriptions.map(function(subscription) {
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined;
|
}
|
||||||
});
|
sendCommand(subscription, command) {
|
||||||
};
|
const {identifier: identifier} = subscription;
|
||||||
Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) {
|
|
||||||
var identifier = subscription.identifier;
|
|
||||||
return this.consumer.send({
|
return this.consumer.send({
|
||||||
command: command,
|
command: command,
|
||||||
identifier: identifier
|
identifier: identifier
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
return Subscriptions;
|
}
|
||||||
}();
|
class Consumer {
|
||||||
var Consumer = function() {
|
constructor(url) {
|
||||||
function Consumer(url) {
|
|
||||||
classCallCheck(this, Consumer);
|
|
||||||
this._url = url;
|
this._url = url;
|
||||||
this.subscriptions = new Subscriptions(this);
|
this.subscriptions = new Subscriptions(this);
|
||||||
this.connection = new Connection(this);
|
this.connection = new Connection(this);
|
||||||
}
|
}
|
||||||
Consumer.prototype.send = function send(data) {
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
return this.connection.send(data);
|
return this.connection.send(data);
|
||||||
};
|
}
|
||||||
Consumer.prototype.connect = function connect() {
|
connect() {
|
||||||
return this.connection.open();
|
return this.connection.open();
|
||||||
};
|
}
|
||||||
Consumer.prototype.disconnect = function disconnect() {
|
disconnect() {
|
||||||
return this.connection.close({
|
return this.connection.close({
|
||||||
allowReconnect: false
|
allowReconnect: false
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() {
|
ensureActiveConnection() {
|
||||||
if (!this.connection.isActive()) {
|
if (!this.connection.isActive()) {
|
||||||
return this.connection.open();
|
return this.connection.open();
|
||||||
}
|
}
|
||||||
};
|
|
||||||
createClass(Consumer, [ {
|
|
||||||
key: "url",
|
|
||||||
get: function get$$1() {
|
|
||||||
return createWebSocketURL(this._url);
|
|
||||||
}
|
}
|
||||||
} ]);
|
}
|
||||||
return Consumer;
|
|
||||||
}();
|
|
||||||
function createWebSocketURL(url) {
|
function createWebSocketURL(url) {
|
||||||
if (typeof url === "function") {
|
if (typeof url === "function") {
|
||||||
url = url();
|
url = url();
|
||||||
}
|
}
|
||||||
if (url && !/^wss?:/i.test(url)) {
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
var a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = url;
|
a.href = url;
|
||||||
a.href = a.href;
|
a.href = a.href;
|
||||||
a.protocol = a.protocol.replace("http", "ws");
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
@ -491,16 +414,16 @@
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function createConsumer() {
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
|
|
||||||
return new Consumer(url);
|
return new Consumer(url);
|
||||||
}
|
}
|
||||||
function getConfig(name) {
|
function getConfig(name) {
|
||||||
var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
if (element) {
|
if (element) {
|
||||||
return element.getAttribute("content");
|
return element.getAttribute("content");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8");
|
||||||
exports.Connection = Connection;
|
exports.Connection = Connection;
|
||||||
exports.ConnectionMonitor = ConnectionMonitor;
|
exports.ConnectionMonitor = ConnectionMonitor;
|
||||||
exports.Consumer = Consumer;
|
exports.Consumer = Consumer;
|
||||||
@ -508,11 +431,11 @@
|
|||||||
exports.Subscription = Subscription;
|
exports.Subscription = Subscription;
|
||||||
exports.Subscriptions = Subscriptions;
|
exports.Subscriptions = Subscriptions;
|
||||||
exports.adapters = adapters;
|
exports.adapters = adapters;
|
||||||
exports.createWebSocketURL = createWebSocketURL;
|
|
||||||
exports.logger = logger;
|
|
||||||
exports.createConsumer = createConsumer;
|
exports.createConsumer = createConsumer;
|
||||||
|
exports.createWebSocketURL = createWebSocketURL;
|
||||||
exports.getConfig = getConfig;
|
exports.getConfig = getConfig;
|
||||||
|
exports.logger = logger;
|
||||||
Object.defineProperty(exports, "__esModule", {
|
Object.defineProperty(exports, "__esModule", {
|
||||||
value: true
|
value: true
|
||||||
});
|
});
|
||||||
});
|
}));
|
||||||
|
442
actioncable/app/assets/javascripts/actioncable.esm.js
Normal file
442
actioncable/app/assets/javascripts/actioncable.esm.js
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
var adapters = {
|
||||||
|
logger: self.console,
|
||||||
|
WebSocket: self.WebSocket
|
||||||
|
};
|
||||||
|
|
||||||
|
var logger = {
|
||||||
|
log(...messages) {
|
||||||
|
if (this.enabled) {
|
||||||
|
messages.push(Date.now());
|
||||||
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const now = () => (new Date).getTime();
|
||||||
|
|
||||||
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
|
|
||||||
|
class ConnectionMonitor {
|
||||||
|
constructor(connection) {
|
||||||
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
|
this.connection = connection;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.isRunning()) {
|
||||||
|
this.startedAt = now();
|
||||||
|
delete this.stoppedAt;
|
||||||
|
this.startPolling();
|
||||||
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.isRunning()) {
|
||||||
|
this.stoppedAt = now();
|
||||||
|
this.stopPolling();
|
||||||
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log("ConnectionMonitor stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRunning() {
|
||||||
|
return this.startedAt && !this.stoppedAt;
|
||||||
|
}
|
||||||
|
recordPing() {
|
||||||
|
this.pingedAt = now();
|
||||||
|
}
|
||||||
|
recordConnect() {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.recordPing();
|
||||||
|
delete this.disconnectedAt;
|
||||||
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
|
}
|
||||||
|
recordDisconnect() {
|
||||||
|
this.disconnectedAt = now();
|
||||||
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
|
}
|
||||||
|
startPolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
this.poll();
|
||||||
|
}
|
||||||
|
stopPolling() {
|
||||||
|
clearTimeout(this.pollTimeout);
|
||||||
|
}
|
||||||
|
poll() {
|
||||||
|
this.pollTimeout = setTimeout((() => {
|
||||||
|
this.reconnectIfStale();
|
||||||
|
this.poll();
|
||||||
|
}), this.getPollInterval());
|
||||||
|
}
|
||||||
|
getPollInterval() {
|
||||||
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
|
const jitter = jitterMax * Math.random();
|
||||||
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
|
}
|
||||||
|
reconnectIfStale() {
|
||||||
|
if (this.connectionIsStale()) {
|
||||||
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.disconnectedRecently()) {
|
||||||
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
|
} else {
|
||||||
|
logger.log("ConnectionMonitor reopening");
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get refreshedAt() {
|
||||||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
|
}
|
||||||
|
connectionIsStale() {
|
||||||
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
disconnectedRecently() {
|
||||||
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
visibilityDidChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
setTimeout((() => {
|
||||||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
|
|
||||||
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
|
|
||||||
|
var INTERNAL = {
|
||||||
|
message_types: {
|
||||||
|
welcome: "welcome",
|
||||||
|
disconnect: "disconnect",
|
||||||
|
ping: "ping",
|
||||||
|
confirmation: "confirm_subscription",
|
||||||
|
rejection: "reject_subscription"
|
||||||
|
},
|
||||||
|
disconnect_reasons: {
|
||||||
|
unauthorized: "unauthorized",
|
||||||
|
invalid_request: "invalid_request",
|
||||||
|
server_restart: "server_restart"
|
||||||
|
},
|
||||||
|
default_mount_path: "/cable",
|
||||||
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
||||||
|
};
|
||||||
|
|
||||||
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
|
|
||||||
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
|
|
||||||
|
const indexOf = [].indexOf;
|
||||||
|
|
||||||
|
class Connection {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.open = this.open.bind(this);
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
|
this.monitor = new ConnectionMonitor(this);
|
||||||
|
this.disconnected = true;
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this.webSocket.send(JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open() {
|
||||||
|
if (this.isActive()) {
|
||||||
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.uninstallEventHandlers();
|
||||||
|
}
|
||||||
|
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
|
||||||
|
this.installEventHandlers();
|
||||||
|
this.monitor.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close({allowReconnect: allowReconnect} = {
|
||||||
|
allowReconnect: true
|
||||||
|
}) {
|
||||||
|
if (!allowReconnect) {
|
||||||
|
this.monitor.stop();
|
||||||
|
}
|
||||||
|
if (this.isActive()) {
|
||||||
|
return this.webSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reopen() {
|
||||||
|
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
||||||
|
if (this.isActive()) {
|
||||||
|
try {
|
||||||
|
return this.close();
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to reopen WebSocket", error);
|
||||||
|
} finally {
|
||||||
|
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
||||||
|
setTimeout(this.open, this.constructor.reopenDelay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getProtocol() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
return this.webSocket.protocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen() {
|
||||||
|
return this.isState("open");
|
||||||
|
}
|
||||||
|
isActive() {
|
||||||
|
return this.isState("open", "connecting");
|
||||||
|
}
|
||||||
|
isProtocolSupported() {
|
||||||
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
|
}
|
||||||
|
isState(...states) {
|
||||||
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
|
}
|
||||||
|
getState() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
for (let state in adapters.WebSocket) {
|
||||||
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
|
return state.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
installEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
const handler = this.events[eventName].bind(this);
|
||||||
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connection.reopenDelay = 500;
|
||||||
|
|
||||||
|
Connection.prototype.events = {
|
||||||
|
message(event) {
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
|
switch (type) {
|
||||||
|
case message_types.welcome:
|
||||||
|
this.monitor.recordConnect();
|
||||||
|
return this.subscriptions.reload();
|
||||||
|
|
||||||
|
case message_types.disconnect:
|
||||||
|
logger.log(`Disconnecting. Reason: ${reason}`);
|
||||||
|
return this.close({
|
||||||
|
allowReconnect: reconnect
|
||||||
|
});
|
||||||
|
|
||||||
|
case message_types.ping:
|
||||||
|
return this.monitor.recordPing();
|
||||||
|
|
||||||
|
case message_types.confirmation:
|
||||||
|
return this.subscriptions.notify(identifier, "connected");
|
||||||
|
|
||||||
|
case message_types.rejection:
|
||||||
|
return this.subscriptions.reject(identifier);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return this.subscriptions.notify(identifier, "received", message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
open() {
|
||||||
|
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
||||||
|
this.disconnected = false;
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
||||||
|
return this.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(event) {
|
||||||
|
logger.log("WebSocket onclose event");
|
||||||
|
if (this.disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disconnected = true;
|
||||||
|
this.monitor.recordDisconnect();
|
||||||
|
return this.subscriptions.notifyAll("disconnected", {
|
||||||
|
willAttemptReconnect: this.monitor.isRunning()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
logger.log("WebSocket onerror event");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extend = function(object, properties) {
|
||||||
|
if (properties != null) {
|
||||||
|
for (let key in properties) {
|
||||||
|
const value = properties[key];
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Subscription {
|
||||||
|
constructor(consumer, params = {}, mixin) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.identifier = JSON.stringify(params);
|
||||||
|
extend(this, mixin);
|
||||||
|
}
|
||||||
|
perform(action, data = {}) {
|
||||||
|
data.action = action;
|
||||||
|
return this.send(data);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.consumer.send({
|
||||||
|
command: "message",
|
||||||
|
identifier: this.identifier,
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unsubscribe() {
|
||||||
|
return this.consumer.subscriptions.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Subscriptions {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
create(channelName, mixin) {
|
||||||
|
const channel = channelName;
|
||||||
|
const params = typeof channel === "object" ? channel : {
|
||||||
|
channel: channel
|
||||||
|
};
|
||||||
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
|
return this.add(subscription);
|
||||||
|
}
|
||||||
|
add(subscription) {
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
this.consumer.ensureActiveConnection();
|
||||||
|
this.notify(subscription, "initialized");
|
||||||
|
this.sendCommand(subscription, "subscribe");
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
remove(subscription) {
|
||||||
|
this.forget(subscription);
|
||||||
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
reject(identifier) {
|
||||||
|
return this.findAll(identifier).map((subscription => {
|
||||||
|
this.forget(subscription);
|
||||||
|
this.notify(subscription, "rejected");
|
||||||
|
return subscription;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
forget(subscription) {
|
||||||
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
findAll(identifier) {
|
||||||
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
|
}
|
||||||
|
reload() {
|
||||||
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
|
}
|
||||||
|
notifyAll(callbackName, ...args) {
|
||||||
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
|
}
|
||||||
|
notify(subscription, callbackName, ...args) {
|
||||||
|
let subscriptions;
|
||||||
|
if (typeof subscription === "string") {
|
||||||
|
subscriptions = this.findAll(subscription);
|
||||||
|
} else {
|
||||||
|
subscriptions = [ subscription ];
|
||||||
|
}
|
||||||
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
|
}
|
||||||
|
sendCommand(subscription, command) {
|
||||||
|
const {identifier: identifier} = subscription;
|
||||||
|
return this.consumer.send({
|
||||||
|
command: command,
|
||||||
|
identifier: identifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Consumer {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
this.subscriptions = new Subscriptions(this);
|
||||||
|
this.connection = new Connection(this);
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.connection.send(data);
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
return this.connection.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ensureActiveConnection() {
|
||||||
|
if (!this.connection.isActive()) {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWebSocketURL(url) {
|
||||||
|
if (typeof url === "function") {
|
||||||
|
url = url();
|
||||||
|
}
|
||||||
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.href = a.href;
|
||||||
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
|
return a.href;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
|
return new Consumer(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getConfig(name) {
|
||||||
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
|
if (element) {
|
||||||
|
return element.getAttribute("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger };
|
440
actioncable/app/assets/javascripts/actioncable.js
Normal file
440
actioncable/app/assets/javascripts/actioncable.js
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
factory(global.ActionCable = {}));
|
||||||
|
})(this, (function(exports) {
|
||||||
|
"use strict";
|
||||||
|
var adapters = {
|
||||||
|
logger: self.console,
|
||||||
|
WebSocket: self.WebSocket
|
||||||
|
};
|
||||||
|
var logger = {
|
||||||
|
log(...messages) {
|
||||||
|
if (this.enabled) {
|
||||||
|
messages.push(Date.now());
|
||||||
|
adapters.logger.log("[ActionCable]", ...messages);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const now = () => (new Date).getTime();
|
||||||
|
const secondsSince = time => (now() - time) / 1e3;
|
||||||
|
class ConnectionMonitor {
|
||||||
|
constructor(connection) {
|
||||||
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||||||
|
this.connection = connection;
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
if (!this.isRunning()) {
|
||||||
|
this.startedAt = now();
|
||||||
|
delete this.stoppedAt;
|
||||||
|
this.startPolling();
|
||||||
|
addEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
if (this.isRunning()) {
|
||||||
|
this.stoppedAt = now();
|
||||||
|
this.stopPolling();
|
||||||
|
removeEventListener("visibilitychange", this.visibilityDidChange);
|
||||||
|
logger.log("ConnectionMonitor stopped");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isRunning() {
|
||||||
|
return this.startedAt && !this.stoppedAt;
|
||||||
|
}
|
||||||
|
recordPing() {
|
||||||
|
this.pingedAt = now();
|
||||||
|
}
|
||||||
|
recordConnect() {
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
this.recordPing();
|
||||||
|
delete this.disconnectedAt;
|
||||||
|
logger.log("ConnectionMonitor recorded connect");
|
||||||
|
}
|
||||||
|
recordDisconnect() {
|
||||||
|
this.disconnectedAt = now();
|
||||||
|
logger.log("ConnectionMonitor recorded disconnect");
|
||||||
|
}
|
||||||
|
startPolling() {
|
||||||
|
this.stopPolling();
|
||||||
|
this.poll();
|
||||||
|
}
|
||||||
|
stopPolling() {
|
||||||
|
clearTimeout(this.pollTimeout);
|
||||||
|
}
|
||||||
|
poll() {
|
||||||
|
this.pollTimeout = setTimeout((() => {
|
||||||
|
this.reconnectIfStale();
|
||||||
|
this.poll();
|
||||||
|
}), this.getPollInterval());
|
||||||
|
}
|
||||||
|
getPollInterval() {
|
||||||
|
const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor;
|
||||||
|
const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10));
|
||||||
|
const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate;
|
||||||
|
const jitter = jitterMax * Math.random();
|
||||||
|
return staleThreshold * 1e3 * backoff * (1 + jitter);
|
||||||
|
}
|
||||||
|
reconnectIfStale() {
|
||||||
|
if (this.connectionIsStale()) {
|
||||||
|
logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`);
|
||||||
|
this.reconnectAttempts++;
|
||||||
|
if (this.disconnectedRecently()) {
|
||||||
|
logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`);
|
||||||
|
} else {
|
||||||
|
logger.log("ConnectionMonitor reopening");
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get refreshedAt() {
|
||||||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||||||
|
}
|
||||||
|
connectionIsStale() {
|
||||||
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
disconnectedRecently() {
|
||||||
|
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
||||||
|
}
|
||||||
|
visibilityDidChange() {
|
||||||
|
if (document.visibilityState === "visible") {
|
||||||
|
setTimeout((() => {
|
||||||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||||||
|
logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`);
|
||||||
|
this.connection.reopen();
|
||||||
|
}
|
||||||
|
}), 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConnectionMonitor.staleThreshold = 6;
|
||||||
|
ConnectionMonitor.reconnectionBackoffRate = .15;
|
||||||
|
var INTERNAL = {
|
||||||
|
message_types: {
|
||||||
|
welcome: "welcome",
|
||||||
|
disconnect: "disconnect",
|
||||||
|
ping: "ping",
|
||||||
|
confirmation: "confirm_subscription",
|
||||||
|
rejection: "reject_subscription"
|
||||||
|
},
|
||||||
|
disconnect_reasons: {
|
||||||
|
unauthorized: "unauthorized",
|
||||||
|
invalid_request: "invalid_request",
|
||||||
|
server_restart: "server_restart"
|
||||||
|
},
|
||||||
|
default_mount_path: "/cable",
|
||||||
|
protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
|
||||||
|
};
|
||||||
|
const {message_types: message_types, protocols: protocols} = INTERNAL;
|
||||||
|
const supportedProtocols = protocols.slice(0, protocols.length - 1);
|
||||||
|
const indexOf = [].indexOf;
|
||||||
|
class Connection {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.open = this.open.bind(this);
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = this.consumer.subscriptions;
|
||||||
|
this.monitor = new ConnectionMonitor(this);
|
||||||
|
this.disconnected = true;
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this.webSocket.send(JSON.stringify(data));
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open() {
|
||||||
|
if (this.isActive()) {
|
||||||
|
logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`);
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`);
|
||||||
|
if (this.webSocket) {
|
||||||
|
this.uninstallEventHandlers();
|
||||||
|
}
|
||||||
|
this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
|
||||||
|
this.installEventHandlers();
|
||||||
|
this.monitor.start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close({allowReconnect: allowReconnect} = {
|
||||||
|
allowReconnect: true
|
||||||
|
}) {
|
||||||
|
if (!allowReconnect) {
|
||||||
|
this.monitor.stop();
|
||||||
|
}
|
||||||
|
if (this.isActive()) {
|
||||||
|
return this.webSocket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reopen() {
|
||||||
|
logger.log(`Reopening WebSocket, current state is ${this.getState()}`);
|
||||||
|
if (this.isActive()) {
|
||||||
|
try {
|
||||||
|
return this.close();
|
||||||
|
} catch (error) {
|
||||||
|
logger.log("Failed to reopen WebSocket", error);
|
||||||
|
} finally {
|
||||||
|
logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`);
|
||||||
|
setTimeout(this.open, this.constructor.reopenDelay);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getProtocol() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
return this.webSocket.protocol;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isOpen() {
|
||||||
|
return this.isState("open");
|
||||||
|
}
|
||||||
|
isActive() {
|
||||||
|
return this.isState("open", "connecting");
|
||||||
|
}
|
||||||
|
isProtocolSupported() {
|
||||||
|
return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
|
||||||
|
}
|
||||||
|
isState(...states) {
|
||||||
|
return indexOf.call(states, this.getState()) >= 0;
|
||||||
|
}
|
||||||
|
getState() {
|
||||||
|
if (this.webSocket) {
|
||||||
|
for (let state in adapters.WebSocket) {
|
||||||
|
if (adapters.WebSocket[state] === this.webSocket.readyState) {
|
||||||
|
return state.toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
installEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
const handler = this.events[eventName].bind(this);
|
||||||
|
this.webSocket[`on${eventName}`] = handler;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uninstallEventHandlers() {
|
||||||
|
for (let eventName in this.events) {
|
||||||
|
this.webSocket[`on${eventName}`] = function() {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Connection.reopenDelay = 500;
|
||||||
|
Connection.prototype.events = {
|
||||||
|
message(event) {
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data);
|
||||||
|
switch (type) {
|
||||||
|
case message_types.welcome:
|
||||||
|
this.monitor.recordConnect();
|
||||||
|
return this.subscriptions.reload();
|
||||||
|
|
||||||
|
case message_types.disconnect:
|
||||||
|
logger.log(`Disconnecting. Reason: ${reason}`);
|
||||||
|
return this.close({
|
||||||
|
allowReconnect: reconnect
|
||||||
|
});
|
||||||
|
|
||||||
|
case message_types.ping:
|
||||||
|
return this.monitor.recordPing();
|
||||||
|
|
||||||
|
case message_types.confirmation:
|
||||||
|
return this.subscriptions.notify(identifier, "connected");
|
||||||
|
|
||||||
|
case message_types.rejection:
|
||||||
|
return this.subscriptions.reject(identifier);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return this.subscriptions.notify(identifier, "received", message);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
open() {
|
||||||
|
logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`);
|
||||||
|
this.disconnected = false;
|
||||||
|
if (!this.isProtocolSupported()) {
|
||||||
|
logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
||||||
|
return this.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
close(event) {
|
||||||
|
logger.log("WebSocket onclose event");
|
||||||
|
if (this.disconnected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.disconnected = true;
|
||||||
|
this.monitor.recordDisconnect();
|
||||||
|
return this.subscriptions.notifyAll("disconnected", {
|
||||||
|
willAttemptReconnect: this.monitor.isRunning()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error() {
|
||||||
|
logger.log("WebSocket onerror event");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const extend = function(object, properties) {
|
||||||
|
if (properties != null) {
|
||||||
|
for (let key in properties) {
|
||||||
|
const value = properties[key];
|
||||||
|
object[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return object;
|
||||||
|
};
|
||||||
|
class Subscription {
|
||||||
|
constructor(consumer, params = {}, mixin) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.identifier = JSON.stringify(params);
|
||||||
|
extend(this, mixin);
|
||||||
|
}
|
||||||
|
perform(action, data = {}) {
|
||||||
|
data.action = action;
|
||||||
|
return this.send(data);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.consumer.send({
|
||||||
|
command: "message",
|
||||||
|
identifier: this.identifier,
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unsubscribe() {
|
||||||
|
return this.consumer.subscriptions.remove(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Subscriptions {
|
||||||
|
constructor(consumer) {
|
||||||
|
this.consumer = consumer;
|
||||||
|
this.subscriptions = [];
|
||||||
|
}
|
||||||
|
create(channelName, mixin) {
|
||||||
|
const channel = channelName;
|
||||||
|
const params = typeof channel === "object" ? channel : {
|
||||||
|
channel: channel
|
||||||
|
};
|
||||||
|
const subscription = new Subscription(this.consumer, params, mixin);
|
||||||
|
return this.add(subscription);
|
||||||
|
}
|
||||||
|
add(subscription) {
|
||||||
|
this.subscriptions.push(subscription);
|
||||||
|
this.consumer.ensureActiveConnection();
|
||||||
|
this.notify(subscription, "initialized");
|
||||||
|
this.sendCommand(subscription, "subscribe");
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
remove(subscription) {
|
||||||
|
this.forget(subscription);
|
||||||
|
if (!this.findAll(subscription.identifier).length) {
|
||||||
|
this.sendCommand(subscription, "unsubscribe");
|
||||||
|
}
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
reject(identifier) {
|
||||||
|
return this.findAll(identifier).map((subscription => {
|
||||||
|
this.forget(subscription);
|
||||||
|
this.notify(subscription, "rejected");
|
||||||
|
return subscription;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
forget(subscription) {
|
||||||
|
this.subscriptions = this.subscriptions.filter((s => s !== subscription));
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
findAll(identifier) {
|
||||||
|
return this.subscriptions.filter((s => s.identifier === identifier));
|
||||||
|
}
|
||||||
|
reload() {
|
||||||
|
return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe")));
|
||||||
|
}
|
||||||
|
notifyAll(callbackName, ...args) {
|
||||||
|
return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args)));
|
||||||
|
}
|
||||||
|
notify(subscription, callbackName, ...args) {
|
||||||
|
let subscriptions;
|
||||||
|
if (typeof subscription === "string") {
|
||||||
|
subscriptions = this.findAll(subscription);
|
||||||
|
} else {
|
||||||
|
subscriptions = [ subscription ];
|
||||||
|
}
|
||||||
|
return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined));
|
||||||
|
}
|
||||||
|
sendCommand(subscription, command) {
|
||||||
|
const {identifier: identifier} = subscription;
|
||||||
|
return this.consumer.send({
|
||||||
|
command: command,
|
||||||
|
identifier: identifier
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class Consumer {
|
||||||
|
constructor(url) {
|
||||||
|
this._url = url;
|
||||||
|
this.subscriptions = new Subscriptions(this);
|
||||||
|
this.connection = new Connection(this);
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return createWebSocketURL(this._url);
|
||||||
|
}
|
||||||
|
send(data) {
|
||||||
|
return this.connection.send(data);
|
||||||
|
}
|
||||||
|
connect() {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
disconnect() {
|
||||||
|
return this.connection.close({
|
||||||
|
allowReconnect: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ensureActiveConnection() {
|
||||||
|
if (!this.connection.isActive()) {
|
||||||
|
return this.connection.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createWebSocketURL(url) {
|
||||||
|
if (typeof url === "function") {
|
||||||
|
url = url();
|
||||||
|
}
|
||||||
|
if (url && !/^wss?:/i.test(url)) {
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.href = a.href;
|
||||||
|
a.protocol = a.protocol.replace("http", "ws");
|
||||||
|
return a.href;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) {
|
||||||
|
return new Consumer(url);
|
||||||
|
}
|
||||||
|
function getConfig(name) {
|
||||||
|
const element = document.head.querySelector(`meta[name='action-cable-${name}']`);
|
||||||
|
if (element) {
|
||||||
|
return element.getAttribute("content");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
exports.Connection = Connection;
|
||||||
|
exports.ConnectionMonitor = ConnectionMonitor;
|
||||||
|
exports.Consumer = Consumer;
|
||||||
|
exports.INTERNAL = INTERNAL;
|
||||||
|
exports.Subscription = Subscription;
|
||||||
|
exports.Subscriptions = Subscriptions;
|
||||||
|
exports.adapters = adapters;
|
||||||
|
exports.createConsumer = createConsumer;
|
||||||
|
exports.createWebSocketURL = createWebSocketURL;
|
||||||
|
exports.getConfig = getConfig;
|
||||||
|
exports.logger = logger;
|
||||||
|
Object.defineProperty(exports, "__esModule", {
|
||||||
|
value: true
|
||||||
|
});
|
||||||
|
}));
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./index"
|
||||||
|
console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8")
|
@ -27,7 +27,6 @@ if (process.env.CI) {
|
|||||||
sl_ff: sauce("firefox", 63),
|
sl_ff: sauce("firefox", 63),
|
||||||
sl_safari: sauce("safari", 12.0, "macOS 10.13"),
|
sl_safari: sauce("safari", 12.0, "macOS 10.13"),
|
||||||
sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
|
sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"),
|
||||||
sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
config.browsers = Object.keys(config.customLaunchers)
|
config.browsers = Object.keys(config.customLaunchers)
|
||||||
|
@ -25,7 +25,7 @@ def broadcasting_for(model)
|
|||||||
serialize_broadcasting([ channel_name, model ])
|
serialize_broadcasting([ channel_name, model ])
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialize_broadcasting(object) #:nodoc:
|
def serialize_broadcasting(object) # :nodoc:
|
||||||
case
|
case
|
||||||
when object.is_a?(Array)
|
when object.is_a?(Array)
|
||||||
object.map { |m| serialize_broadcasting(m) }.join(":")
|
object.map { |m| serialize_broadcasting(m) }.join(":")
|
||||||
|
@ -68,7 +68,7 @@ def initialize(server, env, coder: ActiveSupport::JSON)
|
|||||||
|
|
||||||
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
|
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
|
||||||
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
|
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
|
||||||
def process #:nodoc:
|
def process # :nodoc:
|
||||||
logger.info started_request_message
|
logger.info started_request_message
|
||||||
|
|
||||||
if websocket.possible? && allow_request_origin?
|
if websocket.possible? && allow_request_origin?
|
||||||
@ -80,11 +80,11 @@ def process #:nodoc:
|
|||||||
|
|
||||||
# Decodes WebSocket messages and dispatches them to subscribed channels.
|
# Decodes WebSocket messages and dispatches them to subscribed channels.
|
||||||
# WebSocket message transfer encoding is always JSON.
|
# WebSocket message transfer encoding is always JSON.
|
||||||
def receive(websocket_message) #:nodoc:
|
def receive(websocket_message) # :nodoc:
|
||||||
send_async :dispatch_websocket_message, websocket_message
|
send_async :dispatch_websocket_message, websocket_message
|
||||||
end
|
end
|
||||||
|
|
||||||
def dispatch_websocket_message(websocket_message) #:nodoc:
|
def dispatch_websocket_message(websocket_message) # :nodoc:
|
||||||
if websocket.alive?
|
if websocket.alive?
|
||||||
subscriptions.execute_command decode(websocket_message)
|
subscriptions.execute_command decode(websocket_message)
|
||||||
else
|
else
|
||||||
|
@ -18,10 +18,10 @@ def add_tags(*tags)
|
|||||||
@tags = @tags.uniq
|
@tags = @tags.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
def tag(logger)
|
def tag(logger, &block)
|
||||||
if logger.respond_to?(:tagged)
|
if logger.respond_to?(:tagged)
|
||||||
current_tags = tags - logger.formatter.current_tags
|
current_tags = tags - logger.formatter.current_tags
|
||||||
logger.tagged(*current_tags) { yield }
|
logger.tagged(*current_tags, &block)
|
||||||
else
|
else
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
|
@ -9,6 +9,7 @@ module ActionCable
|
|||||||
class Engine < Rails::Engine # :nodoc:
|
class Engine < Rails::Engine # :nodoc:
|
||||||
config.action_cable = ActiveSupport::OrderedOptions.new
|
config.action_cable = ActiveSupport::OrderedOptions.new
|
||||||
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
|
config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path]
|
||||||
|
config.action_cable.precompile_assets = true
|
||||||
|
|
||||||
config.eager_load_namespaces << ActionCable
|
config.eager_load_namespaces << ActionCable
|
||||||
|
|
||||||
@ -22,6 +23,14 @@ class Engine < Rails::Engine # :nodoc:
|
|||||||
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
initializer "action_cable.asset" do
|
||||||
|
config.after_initialize do |app|
|
||||||
|
if Rails.application.config.respond_to?(:assets) && app.config.action_cable.precompile_assets
|
||||||
|
Rails.application.config.assets.precompile += %w( actioncable.js actioncable.esm.js )
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
initializer "action_cable.set_configs" do |app|
|
initializer "action_cable.set_configs" do |app|
|
||||||
options = app.config.action_cable
|
options = app.config.action_cable
|
||||||
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
|
options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development?
|
||||||
|
@ -8,14 +8,15 @@ module ActionCableHelper
|
|||||||
#
|
#
|
||||||
# <head>
|
# <head>
|
||||||
# <%= action_cable_meta_tag %>
|
# <%= action_cable_meta_tag %>
|
||||||
# <%= javascript_include_tag 'application', 'data-turbolinks-track' => 'reload' %>
|
# <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
|
||||||
# </head>
|
# </head>
|
||||||
#
|
#
|
||||||
# This is then used by Action Cable to determine the URL of your WebSocket server.
|
# This is then used by Action Cable to determine the URL of your WebSocket server.
|
||||||
# Your JavaScript can then connect to the server without needing to specify the
|
# Your JavaScript can then connect to the server without needing to specify the
|
||||||
# URL directly:
|
# URL directly:
|
||||||
#
|
#
|
||||||
# window.Cable = require("@rails/actioncable")
|
# import Cable from "@rails/actioncable"
|
||||||
|
# window.Cable = Cable
|
||||||
# window.App = {}
|
# window.App = {}
|
||||||
# App.cable = Cable.createConsumer()
|
# App.cable = Cable.createConsumer()
|
||||||
#
|
#
|
||||||
|
@ -40,7 +40,7 @@ def initialize(server, broadcasting, coder:)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def broadcast(message)
|
def broadcast(message)
|
||||||
server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}" }
|
server.logger.debug { "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect.truncate(300)}" }
|
||||||
|
|
||||||
payload = { broadcasting: broadcasting, message: message, coder: coder }
|
payload = { broadcasting: broadcasting, message: message, coder: coder }
|
||||||
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
|
ActiveSupport::Notifications.instrument("broadcast.action_cable", payload) do
|
||||||
|
@ -9,6 +9,7 @@ class Configuration
|
|||||||
attr_accessor :connection_class, :worker_pool_size
|
attr_accessor :connection_class, :worker_pool_size
|
||||||
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
|
attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host
|
||||||
attr_accessor :cable, :url, :mount_path
|
attr_accessor :cable, :url, :mount_path
|
||||||
|
attr_accessor :precompile_assets
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
@log_tags = []
|
@log_tags = []
|
||||||
|
@ -36,12 +36,10 @@ def stopping?
|
|||||||
@executor.shuttingdown?
|
@executor.shuttingdown?
|
||||||
end
|
end
|
||||||
|
|
||||||
def work(connection)
|
def work(connection, &block)
|
||||||
self.connection = connection
|
self.connection = connection
|
||||||
|
|
||||||
run_callbacks :work do
|
run_callbacks :work, &block
|
||||||
yield
|
|
||||||
end
|
|
||||||
ensure
|
ensure
|
||||||
self.connection = nil
|
self.connection = nil
|
||||||
end
|
end
|
||||||
|
@ -12,8 +12,8 @@ module ActiveRecordConnectionManagement
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def with_database_connections
|
def with_database_connections(&block)
|
||||||
connection.logger.tag(ActiveRecord::Base.logger) { yield }
|
connection.logger.tag(ActiveRecord::Base.logger, &block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
gem "pg", "~> 1.1"
|
gem "pg", "~> 1.1"
|
||||||
require "pg"
|
require "pg"
|
||||||
require "thread"
|
require "thread"
|
||||||
require "digest/sha1"
|
require "openssl"
|
||||||
|
|
||||||
module ActionCable
|
module ActionCable
|
||||||
module SubscriptionAdapter
|
module SubscriptionAdapter
|
||||||
@ -58,7 +58,7 @@ def with_broadcast_connection(&block) # :nodoc:
|
|||||||
|
|
||||||
private
|
private
|
||||||
def channel_identifier(channel)
|
def channel_identifier(channel)
|
||||||
channel.size > 63 ? Digest::SHA1.hexdigest(channel) : channel
|
channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
|
||||||
end
|
end
|
||||||
|
|
||||||
def listener
|
def listener
|
||||||
|
@ -45,7 +45,7 @@ def after_teardown # :nodoc:
|
|||||||
def assert_broadcasts(stream, number, &block)
|
def assert_broadcasts(stream, number, &block)
|
||||||
if block_given?
|
if block_given?
|
||||||
original_count = broadcasts_size(stream)
|
original_count = broadcasts_size(stream)
|
||||||
assert_nothing_raised(&block)
|
_assert_nothing_raised_or_warn("assert_broadcasts", &block)
|
||||||
new_count = broadcasts_size(stream)
|
new_count = broadcasts_size(stream)
|
||||||
actual_count = new_count - original_count
|
actual_count = new_count - original_count
|
||||||
else
|
else
|
||||||
@ -106,7 +106,7 @@ def assert_broadcast_on(stream, data, &block)
|
|||||||
old_messages = new_messages
|
old_messages = new_messages
|
||||||
clear_messages(stream)
|
clear_messages(stream)
|
||||||
|
|
||||||
assert_nothing_raised(&block)
|
_assert_nothing_raised_or_warn("assert_broadcast_on", &block)
|
||||||
new_messages = broadcasts(stream)
|
new_messages = broadcasts(stream)
|
||||||
clear_messages(stream)
|
clear_messages(stream)
|
||||||
|
|
||||||
|
@ -10,4 +10,4 @@ Example:
|
|||||||
creates a Chat channel class, test and JavaScript asset:
|
creates a Chat channel class, test and JavaScript asset:
|
||||||
Channel: app/channels/chat_channel.rb
|
Channel: app/channels/chat_channel.rb
|
||||||
Test: test/channels/chat_channel_test.rb
|
Test: test/channels/chat_channel_test.rb
|
||||||
Assets: app/javascript/channels/chat_channel.js
|
Assets: $JAVASCRIPT_PATH/channels/chat_channel.js
|
||||||
|
@ -13,39 +13,98 @@ class ChannelGenerator < NamedBase
|
|||||||
|
|
||||||
hook_for :test_framework
|
hook_for :test_framework
|
||||||
|
|
||||||
def create_channel_file
|
def create_channel_files
|
||||||
template "channel.rb", File.join("app/channels", class_path, "#{file_name}_channel.rb")
|
create_shared_channel_files
|
||||||
|
create_channel_file
|
||||||
|
|
||||||
if options[:assets]
|
if using_javascript?
|
||||||
if behavior == :invoke
|
if first_setup_required?
|
||||||
|
create_shared_channel_javascript_files
|
||||||
|
import_channels_in_javascript_entrypoint
|
||||||
|
|
||||||
|
if using_importmap?
|
||||||
|
pin_javascript_dependencies
|
||||||
|
elsif using_node?
|
||||||
|
install_javascript_dependencies
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
create_channel_javascript_file
|
||||||
|
import_channel_in_javascript_entrypoint
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def create_shared_channel_files
|
||||||
|
return if behavior != :invoke
|
||||||
|
|
||||||
|
copy_file "#{__dir__}/templates/application_cable/channel.rb",
|
||||||
|
"app/channels/application_cable/channel.rb"
|
||||||
|
copy_file "#{__dir__}/templates/application_cable/connection.rb",
|
||||||
|
"app/channels/application_cable/connection.rb"
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_channel_file
|
||||||
|
template "channel.rb",
|
||||||
|
File.join("app/channels", class_path, "#{file_name}_channel.rb")
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_shared_channel_javascript_files
|
||||||
template "javascript/index.js", "app/javascript/channels/index.js"
|
template "javascript/index.js", "app/javascript/channels/index.js"
|
||||||
template "javascript/consumer.js", "app/javascript/channels/consumer.js"
|
template "javascript/consumer.js", "app/javascript/channels/consumer.js"
|
||||||
end
|
end
|
||||||
|
|
||||||
js_template "javascript/channel", File.join("app/javascript/channels", class_path, "#{file_name}_channel")
|
def create_channel_javascript_file
|
||||||
|
channel_js_path = File.join("app/javascript/channels", class_path, "#{file_name}_channel")
|
||||||
|
js_template "javascript/channel", channel_js_path
|
||||||
|
gsub_file "#{channel_js_path}.js", /\.\/consumer/, "channels/consumer" unless using_node?
|
||||||
end
|
end
|
||||||
|
|
||||||
generate_application_cable_files
|
def import_channels_in_javascript_entrypoint
|
||||||
|
append_to_file "app/javascript/application.js",
|
||||||
|
using_node? ? %(import "./channels"\n) : %(import "channels"\n)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def import_channel_in_javascript_entrypoint
|
||||||
|
append_to_file "app/javascript/channels/index.js",
|
||||||
|
using_node? ? %(import "./#{file_name}_channel"\n) : %(import "channels/#{file_name}_channel"\n)
|
||||||
|
end
|
||||||
|
|
||||||
|
def install_javascript_dependencies
|
||||||
|
say "Installing JavaScript dependencies", :green
|
||||||
|
run "yarn add @rails/actioncable"
|
||||||
|
end
|
||||||
|
|
||||||
|
def pin_javascript_dependencies
|
||||||
|
append_to_file "config/importmap.rb", <<-RUBY
|
||||||
|
pin "@rails/actioncable", to: "actioncable.esm.js"
|
||||||
|
pin_all_from "app/javascript/channels", under: "channels"
|
||||||
|
RUBY
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
def file_name
|
def file_name
|
||||||
@_file_name ||= super.sub(/_channel\z/i, "")
|
@_file_name ||= super.sub(/_channel\z/i, "")
|
||||||
end
|
end
|
||||||
|
|
||||||
# FIXME: Change these files to symlinks once RubyGems 2.5.0 is required.
|
def first_setup_required?
|
||||||
def generate_application_cable_files
|
!root.join("app/javascript/channels/index.js").exist?
|
||||||
return if behavior != :invoke
|
|
||||||
|
|
||||||
files = [
|
|
||||||
"application_cable/channel.rb",
|
|
||||||
"application_cable/connection.rb"
|
|
||||||
]
|
|
||||||
|
|
||||||
files.each do |name|
|
|
||||||
path = File.join("app/channels/", name)
|
|
||||||
template(name, path) if !File.exist?(path)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def using_javascript?
|
||||||
|
@using_javascript ||= options[:assets] && root.join("app/javascript").exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
def using_node?
|
||||||
|
@using_node ||= root.join("package.json").exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
def using_importmap?
|
||||||
|
@using_importmap ||= root.join("config/importmap.rb").exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
def root
|
||||||
|
@root ||= Pathname(destination_root)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,5 +1 @@
|
|||||||
// Load all the channels within this directory and all subdirectories.
|
// Import all the channels to be used by Action Cable
|
||||||
// Channel files must be named *_channel.js.
|
|
||||||
|
|
||||||
const channels = require.context('.', true, /_channel\.js$/)
|
|
||||||
channels.keys().forEach(channels)
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
"name": "@rails/actioncable",
|
"name": "@rails/actioncable",
|
||||||
"version": "7.0.0-alpha",
|
"version": "7.0.0-alpha",
|
||||||
"description": "WebSocket framework for Ruby on Rails.",
|
"description": "WebSocket framework for Ruby on Rails.",
|
||||||
|
"module": "app/javascript/action_cable/index.js",
|
||||||
"main": "app/assets/javascripts/action_cable.js",
|
"main": "app/assets/javascripts/action_cable.js",
|
||||||
"files": [
|
"files": [
|
||||||
"app/assets/javascripts/*.js",
|
"app/assets/javascripts/*.js",
|
||||||
@ -23,9 +24,8 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://rubyonrails.org/",
|
"homepage": "https://rubyonrails.org/",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"babel-core": "^6.25.0",
|
"@rollup/plugin-node-resolve": "^11.0.1",
|
||||||
"babel-plugin-external-helpers": "^6.22.0",
|
"@rollup/plugin-commonjs": "^19.0.1",
|
||||||
"babel-preset-env": "^1.6.0",
|
|
||||||
"eslint": "^4.3.0",
|
"eslint": "^4.3.0",
|
||||||
"eslint-plugin-import": "^2.7.0",
|
"eslint-plugin-import": "^2.7.0",
|
||||||
"karma": "^3.1.1",
|
"karma": "^3.1.1",
|
||||||
@ -34,11 +34,8 @@
|
|||||||
"karma-sauce-launcher": "^1.2.0",
|
"karma-sauce-launcher": "^1.2.0",
|
||||||
"mock-socket": "^2.0.0",
|
"mock-socket": "^2.0.0",
|
||||||
"qunit": "^2.8.0",
|
"qunit": "^2.8.0",
|
||||||
"rollup": "^0.58.2",
|
"rollup": "^2.35.1",
|
||||||
"rollup-plugin-babel": "^3.0.4",
|
"rollup-plugin-terser": "^7.0.2"
|
||||||
"rollup-plugin-commonjs": "^9.1.0",
|
|
||||||
"rollup-plugin-node-resolve": "^3.3.0",
|
|
||||||
"rollup-plugin-uglify": "^3.0.0"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
"prebuild": "yarn lint && bundle exec rake assets:codegen",
|
||||||
|
@ -1,24 +1,47 @@
|
|||||||
import babel from "rollup-plugin-babel"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import uglify from "rollup-plugin-uglify"
|
import { terser } from "rollup-plugin-terser"
|
||||||
|
|
||||||
const uglifyOptions = {
|
const terserOptions = {
|
||||||
mangle: false,
|
mangle: false,
|
||||||
compress: false,
|
compress: false,
|
||||||
output: {
|
format: {
|
||||||
beautify: true,
|
beautify: true,
|
||||||
indent_level: 2
|
indent_level: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {
|
export default [
|
||||||
|
{
|
||||||
input: "app/javascript/action_cable/index.js",
|
input: "app/javascript/action_cable/index.js",
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: "app/assets/javascripts/actioncable.js",
|
||||||
|
format: "umd",
|
||||||
|
name: "ActionCable"
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
file: "app/assets/javascripts/actioncable.esm.js",
|
||||||
|
format: "es"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
resolve(),
|
||||||
|
terser(terserOptions)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
input: "app/javascript/action_cable/index_with_name_deprecation.js",
|
||||||
output: {
|
output: {
|
||||||
file: "app/assets/javascripts/action_cable.js",
|
file: "app/assets/javascripts/action_cable.js",
|
||||||
format: "umd",
|
format: "umd",
|
||||||
name: "ActionCable"
|
name: "ActionCable"
|
||||||
},
|
},
|
||||||
|
breakOnWarning: false,
|
||||||
plugins: [
|
plugins: [
|
||||||
babel(),
|
resolve(),
|
||||||
uglify(uglifyOptions)
|
terser(terserOptions)
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
]
|
@ -1,6 +1,5 @@
|
|||||||
import babel from "rollup-plugin-babel"
|
import commonjs from "@rollup/plugin-commonjs"
|
||||||
import commonjs from "rollup-plugin-commonjs"
|
import resolve from "@rollup/plugin-node-resolve"
|
||||||
import resolve from "rollup-plugin-node-resolve"
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
input: "test/javascript/src/test.js",
|
input: "test/javascript/src/test.js",
|
||||||
@ -12,7 +11,6 @@ export default {
|
|||||||
|
|
||||||
plugins: [
|
plugins: [
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs()
|
||||||
babel()
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -201,7 +201,7 @@ def websocket_client(port)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def concurrently(enum)
|
def concurrently(enum)
|
||||||
enum.map { |*x| Concurrent::Future.execute { yield(*x) } }.map(&:value!)
|
enum.map { |*x| Concurrent::Promises.future { yield(*x) } }.map(&:value!)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_single_client
|
def test_single_client
|
||||||
|
@ -12,8 +12,8 @@ class RedisAdapterTest < ActionCable::TestCase
|
|||||||
|
|
||||||
def cable_config
|
def cable_config
|
||||||
{ adapter: "redis", driver: "ruby" }.tap do |x|
|
{ adapter: "redis", driver: "ruby" }.tap do |x|
|
||||||
if host = URI(ENV["REDIS_URL"] || "").hostname
|
if host = ENV["REDIS_URL"]
|
||||||
x[:host] = host
|
x[:url] = host
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -29,7 +29,8 @@ class RedisAdapterTest::AlternateConfiguration < RedisAdapterTest
|
|||||||
def cable_config
|
def cable_config
|
||||||
alt_cable_config = super.dup
|
alt_cable_config = super.dup
|
||||||
alt_cable_config.delete(:url)
|
alt_cable_config.delete(:url)
|
||||||
alt_cable_config.merge(host: URI(ENV["REDIS_URL"] || "").hostname || "127.0.0.1", port: 6379, db: 12)
|
url = URI(ENV["REDIS_URL"] || "")
|
||||||
|
alt_cable_config.merge(host: url.hostname || "127.0.0.1", port: url.port || 6379, db: 12)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -7,11 +7,6 @@
|
|||||||
require "puma"
|
require "puma"
|
||||||
require "rack/mock"
|
require "rack/mock"
|
||||||
|
|
||||||
begin
|
|
||||||
require "byebug"
|
|
||||||
rescue LoadError
|
|
||||||
end
|
|
||||||
|
|
||||||
# Require all the stubs and models
|
# Require all the stubs and models
|
||||||
Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }
|
Dir[File.expand_path("stubs/*.rb", __dir__)].each { |file| require file }
|
||||||
|
|
||||||
|
@ -1,4 +1,38 @@
|
|||||||
|
* Add `attachments` to the list of permitted parameters for inbound emails conductor.
|
||||||
|
|
||||||
|
When using the conductor to test inbound emails with attachments, this prevents an
|
||||||
|
unpermitted parameter warning in default configurations, and prevents errors for
|
||||||
|
applications that set:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
config.action_controller.action_on_unpermitted_parameters = :raise
|
||||||
|
```
|
||||||
|
|
||||||
|
*David Jones*, *Dana Henke*
|
||||||
|
|
||||||
|
* Add ability to configure ActiveStorage service
|
||||||
|
for storing email raw source.
|
||||||
|
|
||||||
|
```yml
|
||||||
|
# config/storage.yml
|
||||||
|
incoming_emails:
|
||||||
|
service: Disk
|
||||||
|
root: /secure/dir/for/emails/only
|
||||||
|
```
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
config.action_mailbox.storage_service = :incoming_emails
|
||||||
|
```
|
||||||
|
|
||||||
|
*Yurii Rashkovskii*
|
||||||
|
|
||||||
|
* Add ability to incinerate an inbound message through the conductor interface.
|
||||||
|
|
||||||
|
*Santiago Bartesaghi*
|
||||||
|
|
||||||
|
* OpenSSL constants are now used for Digest computations.
|
||||||
|
|
||||||
|
*Dirkjan Bussink*
|
||||||
|
|
||||||
|
|
||||||
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actionmailbox/CHANGELOG.md) for previous changes.
|
Please check [6-1-stable](https://github.com/rails/rails/blob/6-1-stable/actionmailbox/CHANGELOG.md) for previous changes.
|
||||||
|
@ -20,14 +20,18 @@ def create
|
|||||||
|
|
||||||
private
|
private
|
||||||
def new_mail
|
def new_mail
|
||||||
Mail.new(params.require(:mail).permit(:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body).to_h).tap do |mail|
|
Mail.new(mail_params.except(:attachments).to_h).tap do |mail|
|
||||||
mail[:bcc]&.include_in_headers = true
|
mail[:bcc]&.include_in_headers = true
|
||||||
params[:mail][:attachments].to_a.each do |attachment|
|
mail_params[:attachments].to_a.each do |attachment|
|
||||||
mail.add_file(filename: attachment.original_filename, content: attachment.read)
|
mail.add_file(filename: attachment.original_filename, content: attachment.read)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mail_params
|
||||||
|
params.require(:mail).permit(:from, :to, :cc, :bcc, :x_original_to, :in_reply_to, :subject, :body, attachments: [])
|
||||||
|
end
|
||||||
|
|
||||||
def create_inbound_email(mail)
|
def create_inbound_email(mail)
|
||||||
ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
|
ActionMailbox::InboundEmail.create_and_extract_message_id!(mail.to_s)
|
||||||
end
|
end
|
||||||
|
12
actionmailbox/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb
Normal file
12
actionmailbox/app/controllers/rails/conductor/action_mailbox/incinerates_controller.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Rails
|
||||||
|
# Incinerating will destroy an email that is due and has already been processed.
|
||||||
|
class Conductor::ActionMailbox::IncineratesController < Rails::Conductor::BaseController
|
||||||
|
def create
|
||||||
|
ActionMailbox::InboundEmail.find(params[:inbound_email_id]).incinerate
|
||||||
|
|
||||||
|
redirect_to main_app.rails_conductor_inbound_emails_url
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -29,7 +29,7 @@ class InboundEmail < Record
|
|||||||
|
|
||||||
include Incineratable, MessageId, Routable
|
include Incineratable, MessageId, Routable
|
||||||
|
|
||||||
has_one_attached :raw_email
|
has_one_attached :raw_email, service: ActionMailbox.storage_service
|
||||||
enum status: %i[ pending processing delivered failed bounced ]
|
enum status: %i[ pending processing delivered failed bounced ]
|
||||||
|
|
||||||
def mail
|
def mail
|
||||||
|
@ -14,7 +14,7 @@ module ActionMailbox::InboundEmail::MessageId
|
|||||||
# attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
|
# attachment called +raw_email+. Before the upload, extract the Message-ID from the +source+ and set
|
||||||
# it as an attribute on the new +InboundEmail+.
|
# it as an attribute on the new +InboundEmail+.
|
||||||
def create_and_extract_message_id!(source, **options)
|
def create_and_extract_message_id!(source, **options)
|
||||||
message_checksum = Digest::SHA1.hexdigest(source)
|
message_checksum = OpenSSL::Digest::SHA1.hexdigest(source)
|
||||||
message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
|
message_id = extract_message_id(source) || generate_missing_message_id(message_checksum)
|
||||||
|
|
||||||
create! raw_email: create_and_upload_raw_email!(source),
|
create! raw_email: create_and_upload_raw_email!(source),
|
||||||
@ -35,7 +35,8 @@ def generate_missing_message_id(message_checksum)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def create_and_upload_raw_email!(source)
|
def create_and_upload_raw_email!(source)
|
||||||
ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822"
|
ActiveStorage::Blob.create_and_upload! io: StringIO.new(source), filename: "message.eml", content_type: "message/rfc822",
|
||||||
|
service_name: ActionMailbox.storage_service
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionMailbox
|
module ActionMailbox
|
||||||
class Record < ActiveRecord::Base #:nodoc:
|
class Record < ActiveRecord::Base # :nodoc:
|
||||||
self.abstract_class = true
|
self.abstract_class = true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
|
<li><%= button_to "Route again", main_app.rails_conductor_inbound_email_reroute_path(@inbound_email), method: :post %></li>
|
||||||
<li>Incinerate</li>
|
<li><%= button_to "Incinerate", main_app.rails_conductor_inbound_email_incinerate_path(@inbound_email), method: :post %></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
|
@ -21,5 +21,6 @@
|
|||||||
post "inbound_emails/sources", to: "inbound_emails/sources#create", as: :rails_conductor_inbound_email_sources
|
post "inbound_emails/sources", to: "inbound_emails/sources#create", as: :rails_conductor_inbound_email_sources
|
||||||
|
|
||||||
post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
|
post ":inbound_email_id/reroute" => "reroutes#create", as: :rails_conductor_inbound_email_reroute
|
||||||
|
post ":inbound_email_id/incinerate" => "incinerates#create", as: :rails_conductor_inbound_email_incinerate
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -14,4 +14,5 @@ module ActionMailbox
|
|||||||
mattr_accessor :incinerate, default: true
|
mattr_accessor :incinerate, default: true
|
||||||
mattr_accessor :incinerate_after, default: 30.days
|
mattr_accessor :incinerate_after, default: 30.days
|
||||||
mattr_accessor :queues, default: {}
|
mattr_accessor :queues, default: {}
|
||||||
|
mattr_accessor :storage_service
|
||||||
end
|
end
|
||||||
|
@ -77,7 +77,7 @@ def initialize(inbound_email)
|
|||||||
@inbound_email = inbound_email
|
@inbound_email = inbound_email
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform_processing #:nodoc:
|
def perform_processing # :nodoc:
|
||||||
track_status_of_inbound_email do
|
track_status_of_inbound_email do
|
||||||
run_callbacks :process do
|
run_callbacks :process do
|
||||||
process
|
process
|
||||||
@ -92,7 +92,7 @@ def process
|
|||||||
# Overwrite in subclasses
|
# Overwrite in subclasses
|
||||||
end
|
end
|
||||||
|
|
||||||
def finished_processing? #:nodoc:
|
def finished_processing? # :nodoc:
|
||||||
inbound_email.delivered? || inbound_email.bounced?
|
inbound_email.delivered? || inbound_email.bounced?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -20,6 +20,8 @@ class Engine < Rails::Engine
|
|||||||
config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
|
config.action_mailbox.queues = ActiveSupport::InheritableOptions.new \
|
||||||
incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
|
incineration: :action_mailbox_incineration, routing: :action_mailbox_routing
|
||||||
|
|
||||||
|
config.action_mailbox.storage_service = nil
|
||||||
|
|
||||||
initializer "action_mailbox.config" do
|
initializer "action_mailbox.config" do
|
||||||
config.after_initialize do |app|
|
config.after_initialize do |app|
|
||||||
ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
|
ActionMailbox.logger = app.config.action_mailbox.logger || Rails.logger
|
||||||
@ -27,6 +29,7 @@ class Engine < Rails::Engine
|
|||||||
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
|
ActionMailbox.incinerate_after = app.config.action_mailbox.incinerate_after || 30.days
|
||||||
ActionMailbox.queues = app.config.action_mailbox.queues || {}
|
ActionMailbox.queues = app.config.action_mailbox.queues || {}
|
||||||
ActionMailbox.ingress = app.config.action_mailbox.ingress
|
ActionMailbox.ingress = app.config.action_mailbox.ingress
|
||||||
|
ActionMailbox.storage_service = app.config.action_mailbox.storage_service
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
module Dummy
|
module Dummy
|
||||||
class Application < Rails::Application
|
class Application < Rails::Application
|
||||||
# Initialize configuration defaults for originally generated Rails version.
|
# Initialize configuration defaults for originally generated Rails version.
|
||||||
config.load_defaults 6.0
|
config.load_defaults 7.0
|
||||||
|
|
||||||
# Settings in config/environments/* take precedence over those specified here.
|
# Settings in config/environments/* take precedence over those specified here.
|
||||||
# Application configuration can go into files in config/initializers
|
# Application configuration can go into files in config/initializers
|
||||||
|
@ -63,8 +63,4 @@
|
|||||||
|
|
||||||
# Annotate rendered view with file names
|
# Annotate rendered view with file names
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
# Use an evented file watcher to asynchronously detect changes in source code,
|
|
||||||
# routes, locales, etc. This feature depends on the listen gem.
|
|
||||||
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
|
||||||
end
|
end
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
|
||||||
|
|
||||||
# Compress JavaScripts and CSS.
|
# Compress JavaScripts and CSS.
|
||||||
config.assets.js_compressor = :uglifier
|
config.assets.js_compressor = :terser
|
||||||
# config.assets.css_compressor = :sass
|
# config.assets.css_compressor = :sass
|
||||||
|
|
||||||
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
# Do not fallback to assets pipeline if a precompiled asset is missed.
|
||||||
|
@ -46,4 +46,7 @@
|
|||||||
|
|
||||||
# Annotate rendered view with file names
|
# Annotate rendered view with file names
|
||||||
# config.action_view.annotate_rendered_view_with_filenames = true
|
# config.action_view.annotate_rendered_view_with_filenames = true
|
||||||
|
|
||||||
|
# Raise error if unpermitted parameters are sent
|
||||||
|
config.action_controller.action_on_unpermitted_parameters = :raise
|
||||||
end
|
end
|
||||||
|
@ -6,6 +6,10 @@ local:
|
|||||||
service: Disk
|
service: Disk
|
||||||
root: <%= Rails.root.join("storage") %>
|
root: <%= Rails.root.join("storage") %>
|
||||||
|
|
||||||
|
test_email:
|
||||||
|
service: Disk
|
||||||
|
root: <%= Rails.root.join("tmp/storage_email") %>
|
||||||
|
|
||||||
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
|
||||||
# amazon:
|
# amazon:
|
||||||
# service: S3
|
# service: S3
|
||||||
|
@ -7,7 +7,6 @@
|
|||||||
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
ActiveRecord::Migrator.migrations_paths = [File.expand_path("../test/dummy/db/migrate", __dir__)]
|
||||||
require "rails/test_help"
|
require "rails/test_help"
|
||||||
|
|
||||||
require "byebug"
|
|
||||||
require "webmock/minitest"
|
require "webmock/minitest"
|
||||||
|
|
||||||
require "rails/test_unit/reporter"
|
require "rails/test_unit/reporter"
|
||||||
|
@ -44,5 +44,41 @@ class InboundEmailTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "email gets saved to the configured storage service" do
|
||||||
|
ActionMailbox.storage_service = :test_email
|
||||||
|
|
||||||
|
assert_equal(:test_email, ActionMailbox.storage_service)
|
||||||
|
|
||||||
|
email = create_inbound_email_from_fixture("welcome.eml")
|
||||||
|
|
||||||
|
storage_service = ActiveStorage::Blob.services.fetch(ActionMailbox.storage_service)
|
||||||
|
raw = email.raw_email_blob
|
||||||
|
|
||||||
|
# Not present in the main storage
|
||||||
|
assert_not(ActiveStorage::Blob.service.exist?(raw.key))
|
||||||
|
# Present in the email storage
|
||||||
|
assert(storage_service.exist?(raw.key))
|
||||||
|
ensure
|
||||||
|
ActionMailbox.storage_service = nil
|
||||||
|
end
|
||||||
|
|
||||||
|
test "email gets saved to the default storage service, even if it gets changed" do
|
||||||
|
default_service = ActiveStorage::Blob.service
|
||||||
|
ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:test_email)
|
||||||
|
|
||||||
|
# Doesn't change ActionMailbox.storage_service
|
||||||
|
assert_nil(ActionMailbox.storage_service)
|
||||||
|
|
||||||
|
email = create_inbound_email_from_fixture("welcome.eml")
|
||||||
|
raw = email.raw_email_blob
|
||||||
|
|
||||||
|
# Not present in the (previously) default storage
|
||||||
|
assert_not(default_service.exist?(raw.key))
|
||||||
|
# Present in the current default storage (email)
|
||||||
|
assert(ActiveStorage::Blob.service.exist?(raw.key))
|
||||||
|
ensure
|
||||||
|
ActiveStorage::Blob.service = default_service
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -132,11 +132,11 @@ class RouterTest < ActiveSupport::TestCase
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "missing route" do
|
test "missing route" do
|
||||||
assert_raises(ActionMailbox::Router::RoutingError) do
|
|
||||||
inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
|
inbound_email = create_inbound_email_from_mail(to: "going-nowhere@example.com", subject: "This is a reply")
|
||||||
|
assert_raises(ActionMailbox::Router::RoutingError) do
|
||||||
@router.route inbound_email
|
@router.route inbound_email
|
||||||
assert inbound_email.bounced?
|
|
||||||
end
|
end
|
||||||
|
assert inbound_email.bounced?
|
||||||
end
|
end
|
||||||
|
|
||||||
test "invalid address" do
|
test "invalid address" do
|
||||||
|
@ -555,7 +555,7 @@ def default(value = nil)
|
|||||||
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
|
# through a callback when you call <tt>:deliver</tt> on the <tt>Mail::Message</tt>,
|
||||||
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
|
# calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
|
||||||
# nothing except tell the logger you sent the email.
|
# nothing except tell the logger you sent the email.
|
||||||
def deliver_mail(mail) #:nodoc:
|
def deliver_mail(mail) # :nodoc:
|
||||||
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
|
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
|
||||||
set_payload_for_mail(payload, mail)
|
set_payload_for_mail(payload, mail)
|
||||||
yield # Let Mail do the delivery actions
|
yield # Let Mail do the delivery actions
|
||||||
@ -606,7 +606,7 @@ def initialize
|
|||||||
@_message = Mail.new
|
@_message = Mail.new
|
||||||
end
|
end
|
||||||
|
|
||||||
def process(method_name, *args) #:nodoc:
|
def process(method_name, *args) # :nodoc:
|
||||||
payload = {
|
payload = {
|
||||||
mailer: self.class.name,
|
mailer: self.class.name,
|
||||||
action: method_name,
|
action: method_name,
|
||||||
@ -619,7 +619,7 @@ def process(method_name, *args) #:nodoc:
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class NullMail #:nodoc:
|
class NullMail # :nodoc:
|
||||||
def body; "" end
|
def body; "" end
|
||||||
def header; {} end
|
def header; {} end
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ class DeliveryJob < ActiveJob::Base # :nodoc:
|
|||||||
MSG
|
MSG
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(mailer, mail_method, delivery_method, *args) #:nodoc:
|
def perform(mailer, mail_method, delivery_method, *args) # :nodoc:
|
||||||
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
|
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
|
||||||
end
|
end
|
||||||
ruby2_keywords(:perform)
|
ruby2_keywords(:perform)
|
||||||
|
@ -17,15 +17,15 @@ class InlinePreviewInterceptor
|
|||||||
|
|
||||||
include Base64
|
include Base64
|
||||||
|
|
||||||
def self.previewing_email(message) #:nodoc:
|
def self.previewing_email(message) # :nodoc:
|
||||||
new(message).transform!
|
new(message).transform!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(message) #:nodoc:
|
def initialize(message) # :nodoc:
|
||||||
@message = message
|
@message = message
|
||||||
end
|
end
|
||||||
|
|
||||||
def transform! #:nodoc:
|
def transform! # :nodoc:
|
||||||
return message if html_part.blank?
|
return message if html_part.blank?
|
||||||
|
|
||||||
html_part.body = html_part.decoded.gsub(PATTERN) do |match|
|
html_part.body = html_part.decoded.gsub(PATTERN) do |match|
|
||||||
|
@ -15,7 +15,7 @@ module ActionMailer
|
|||||||
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
|
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
|
||||||
# Notifier.welcome(User.first).message # a Mail::Message object
|
# Notifier.welcome(User.first).message # a Mail::Message object
|
||||||
class MessageDelivery < Delegator
|
class MessageDelivery < Delegator
|
||||||
def initialize(mailer_class, action, *args) #:nodoc:
|
def initialize(mailer_class, action, *args) # :nodoc:
|
||||||
@mailer_class, @action, @args = mailer_class, action, args
|
@mailer_class, @action, @args = mailer_class, action, args
|
||||||
|
|
||||||
# The mail is only processed if we try to call any methods on it.
|
# The mail is only processed if we try to call any methods on it.
|
||||||
@ -26,12 +26,12 @@ def initialize(mailer_class, action, *args) #:nodoc:
|
|||||||
ruby2_keywords(:initialize)
|
ruby2_keywords(:initialize)
|
||||||
|
|
||||||
# Method calls are delegated to the Mail::Message that's ready to deliver.
|
# Method calls are delegated to the Mail::Message that's ready to deliver.
|
||||||
def __getobj__ #:nodoc:
|
def __getobj__ # :nodoc:
|
||||||
@mail_message ||= processed_mailer.message
|
@mail_message ||= processed_mailer.message
|
||||||
end
|
end
|
||||||
|
|
||||||
# Unused except for delegator internals (dup, marshalling).
|
# Unused except for delegator internals (dup, marshalling).
|
||||||
def __setobj__(mail_message) #:nodoc:
|
def __setobj__(mail_message) # :nodoc:
|
||||||
@mail_message = mail_message
|
@mail_message = mail_message
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
require "active_support/descendants_tracker"
|
require "active_support/descendants_tracker"
|
||||||
|
|
||||||
module ActionMailer
|
module ActionMailer
|
||||||
module Previews #:nodoc:
|
module Previews # :nodoc:
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionMailer #:nodoc:
|
module ActionMailer # :nodoc:
|
||||||
# Provides +rescue_from+ for mailers. Wraps mailer action processing,
|
# Provides +rescue_from+ for mailers. Wraps mailer action processing,
|
||||||
# mail job processing, and mail delivery.
|
# mail job processing, and mail delivery.
|
||||||
module Rescuable
|
module Rescuable
|
||||||
@ -8,12 +8,12 @@ module Rescuable
|
|||||||
include ActiveSupport::Rescuable
|
include ActiveSupport::Rescuable
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
def handle_exception(exception) #:nodoc:
|
def handle_exception(exception) # :nodoc:
|
||||||
rescue_with_handler(exception) || raise(exception)
|
rescue_with_handler(exception) || raise(exception)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_exceptions #:nodoc:
|
def handle_exceptions # :nodoc:
|
||||||
yield
|
yield
|
||||||
rescue => exception
|
rescue => exception
|
||||||
rescue_with_handler(exception) || raise
|
rescue_with_handler(exception) || raise
|
||||||
|
@ -1,6 +1,81 @@
|
|||||||
* Drop support for the `SERVER_ADDR` header
|
* Use a static error message when raising `ActionDispatch::Http::Parameters::ParseError`
|
||||||
|
to avoid inadvertently logging the HTTP request body at the `fatal` level when it contains
|
||||||
|
malformed JSON.
|
||||||
|
|
||||||
Following up https://github.com/rack/rack/pull/1573 and https://github.com/rails/rails/pull/42349
|
Fixes #41145
|
||||||
|
|
||||||
|
*Aaron Lahey*
|
||||||
|
|
||||||
|
* Add `Middleware#delete!` to delete middleware or raise if not found.
|
||||||
|
|
||||||
|
`Middleware#delete!` works just like `Middleware#delete` but will
|
||||||
|
raise an error if the middleware isn't found.
|
||||||
|
|
||||||
|
*Alex Ghiculescu*, *Petrik de Heus*, *Junichi Sato*
|
||||||
|
|
||||||
|
* Raise error on unpermitted open redirects.
|
||||||
|
|
||||||
|
Add `allow_other_host` options to `redirect_to`.
|
||||||
|
Opt in to this behaviour with `ActionController::Base.raise_on_open_redirects = true`.
|
||||||
|
|
||||||
|
*Gannon McGibbon*
|
||||||
|
|
||||||
|
* Deprecate `poltergeist` and `webkit` (capybara-webkit) driver registration for system testing (they will be removed in Rails 7.1). Add `cuprite` instead.
|
||||||
|
|
||||||
|
[Poltergeist](https://github.com/teampoltergeist/poltergeist) and [capybara-webkit](https://github.com/thoughtbot/capybara-webkit) are already not maintained. These usage in Rails are removed for avoiding confusing users.
|
||||||
|
|
||||||
|
[Cuprite](https://github.com/rubycdp/cuprite) is a good alternative to Poltergeist. Some guide descriptions are replaced from Poltergeist to Cuprite.
|
||||||
|
|
||||||
|
*Yusuke Iwaki*
|
||||||
|
|
||||||
|
* Exclude additional flash types from `ActionController::Base.action_methods`.
|
||||||
|
|
||||||
|
Ensures that additional flash types defined on ActionController::Base subclasses
|
||||||
|
are not listed as actions on that controller.
|
||||||
|
|
||||||
|
class MyController < ApplicationController
|
||||||
|
add_flash_types :hype
|
||||||
|
end
|
||||||
|
|
||||||
|
MyController.action_methods.include?('hype') # => false
|
||||||
|
|
||||||
|
*Gavin Morrice*
|
||||||
|
|
||||||
|
* OpenSSL constants are now used for Digest computations.
|
||||||
|
|
||||||
|
*Dirkjan Bussink*
|
||||||
|
|
||||||
|
* Remove IE6-7-8 file download related hack/fix from ActionController::DataStreaming module.
|
||||||
|
|
||||||
|
Due to the age of those versions of IE this fix is no longer relevant, more importantly it creates an edge-case for unexpected Cache-Control headers.
|
||||||
|
|
||||||
|
*Tadas Sasnauskas*
|
||||||
|
|
||||||
|
* Configuration setting to skip logging an uncaught exception backtrace when the exception is
|
||||||
|
present in `rescued_responses`.
|
||||||
|
|
||||||
|
It may be too noisy to get all backtraces logged for applications that manage uncaught
|
||||||
|
exceptions via `rescued_responses` and `exceptions_app`.
|
||||||
|
`config.action_dispatch.log_rescued_responses` (defaults to `true`) can be set to `false` in
|
||||||
|
this case, so that only exceptions not found in `rescued_responses` will be logged.
|
||||||
|
|
||||||
|
*Alexander Azarov*, *Mike Dalessio*
|
||||||
|
|
||||||
|
* Ignore file fixtures on `db:fixtures:load`.
|
||||||
|
|
||||||
|
*Kevin Sjöberg*
|
||||||
|
|
||||||
|
* Fix ActionController::Live controller test deadlocks by removing the body buffer size limit for tests.
|
||||||
|
|
||||||
|
*Dylan Thacker-Smith*
|
||||||
|
|
||||||
|
* New `ActionController::ConditionalGet#no_store` method to set HTTP cache control `no-store` directive.
|
||||||
|
|
||||||
|
*Tadas Sasnauskas*
|
||||||
|
|
||||||
|
* Drop support for the `SERVER_ADDR` header.
|
||||||
|
|
||||||
|
Following up https://github.com/rack/rack/pull/1573 and https://github.com/rails/rails/pull/42349.
|
||||||
|
|
||||||
*Ricardo Díaz*
|
*Ricardo Díaz*
|
||||||
|
|
||||||
@ -8,7 +83,7 @@
|
|||||||
|
|
||||||
*Gannon McGibbon*
|
*Gannon McGibbon*
|
||||||
|
|
||||||
* Add `cache_control: {}` option to `fresh_when` and `stale?`
|
* Add `cache_control: {}` option to `fresh_when` and `stale?`.
|
||||||
|
|
||||||
Works as a shortcut to set `response.cache_control` with the above methods.
|
Works as a shortcut to set `response.cache_control` with the above methods.
|
||||||
|
|
||||||
@ -22,7 +97,7 @@
|
|||||||
|
|
||||||
* Add support for 'require-trusted-types-for' and 'trusted-types' headers.
|
* Add support for 'require-trusted-types-for' and 'trusted-types' headers.
|
||||||
|
|
||||||
Fixes #42034
|
Fixes #42034.
|
||||||
|
|
||||||
*lfalcao*
|
*lfalcao*
|
||||||
|
|
||||||
@ -91,7 +166,7 @@
|
|||||||
|
|
||||||
*Janko Marohnić*
|
*Janko Marohnić*
|
||||||
|
|
||||||
* Allow anything with `#to_str` (like `Addressable::URI`) as a `redirect_to` location
|
* Allow anything with `#to_str` (like `Addressable::URI`) as a `redirect_to` location.
|
||||||
|
|
||||||
*ojab*
|
*ojab*
|
||||||
|
|
||||||
@ -103,7 +178,7 @@
|
|||||||
as `RemoteIp` middleware behaves inconsistently depending on whether this is configured
|
as `RemoteIp` middleware behaves inconsistently depending on whether this is configured
|
||||||
with a single value or an enumerable.
|
with a single value or an enumerable.
|
||||||
|
|
||||||
Fixes #40772
|
Fixes #40772.
|
||||||
|
|
||||||
*Christian Sutter*
|
*Christian Sutter*
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module AbstractController
|
module AbstractController
|
||||||
module AssetPaths #:nodoc:
|
module AssetPaths # :nodoc:
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
@ -9,37 +9,23 @@
|
|||||||
module AbstractController
|
module AbstractController
|
||||||
# Raised when a non-existing controller action is triggered.
|
# Raised when a non-existing controller action is triggered.
|
||||||
class ActionNotFound < StandardError
|
class ActionNotFound < StandardError
|
||||||
attr_reader :controller, :action
|
attr_reader :controller, :action # :nodoc:
|
||||||
def initialize(message = nil, controller = nil, action = nil)
|
|
||||||
|
def initialize(message = nil, controller = nil, action = nil) # :nodoc:
|
||||||
@controller = controller
|
@controller = controller
|
||||||
@action = action
|
@action = action
|
||||||
super(message)
|
super(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
class Correction
|
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
|
||||||
def initialize(error)
|
include DidYouMean::Correctable # :nodoc:
|
||||||
@error = error
|
|
||||||
end
|
|
||||||
|
|
||||||
def corrections
|
def corrections # :nodoc:
|
||||||
if @error.action
|
@corrections ||= DidYouMean::SpellChecker.new(dictionary: controller.class.action_methods).correct(action)
|
||||||
maybe_these = @error.controller.class.action_methods
|
|
||||||
|
|
||||||
maybe_these.sort_by { |n|
|
|
||||||
DidYouMean::Jaro.distance(@error.action.to_s, n)
|
|
||||||
}.reverse.first(4)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# We may not have DYM, and DYM might not let us register error handlers
|
|
||||||
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
|
|
||||||
DidYouMean.correct_error(self, Correction)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# AbstractController::Base is a low-level API. Nobody should be
|
# AbstractController::Base is a low-level API. Nobody should be
|
||||||
# using it directly, and subclasses (like ActionController::Base) are
|
# using it directly, and subclasses (like ActionController::Base) are
|
||||||
# expected to provide their own +render+ method, since rendering means
|
# expected to provide their own +render+ method, since rendering means
|
||||||
|
@ -142,8 +142,8 @@ def expire_fragment(key, options = nil)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def instrument_fragment_cache(name, key) # :nodoc:
|
def instrument_fragment_cache(name, key, &block) # :nodoc:
|
||||||
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key)) { yield }
|
ActiveSupport::Notifications.instrument("#{name}.#{instrument_name}", instrument_payload(key), &block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -10,6 +10,7 @@ def self.generate_method_for_mime(mime)
|
|||||||
def #{sym}(*args, &block)
|
def #{sym}(*args, &block)
|
||||||
custom(Mime[:#{sym}], *args, &block)
|
custom(Mime[:#{sym}], *args, &block)
|
||||||
end
|
end
|
||||||
|
ruby2_keywords(:#{sym})
|
||||||
RUBY
|
RUBY
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -22,7 +23,7 @@ def #{sym}(*args, &block)
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def method_missing(symbol, &block)
|
def method_missing(symbol, *args, &block)
|
||||||
unless mime_constant = Mime[symbol]
|
unless mime_constant = Mime[symbol]
|
||||||
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
|
raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
|
||||||
"https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
|
"https://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
|
||||||
@ -33,10 +34,11 @@ def method_missing(symbol, &block)
|
|||||||
|
|
||||||
if Mime::SET.include?(mime_constant)
|
if Mime::SET.include?(mime_constant)
|
||||||
AbstractController::Collector.generate_method_for_mime(mime_constant)
|
AbstractController::Collector.generate_method_for_mime(mime_constant)
|
||||||
send(symbol, &block)
|
public_send(symbol, *args, &block)
|
||||||
else
|
else
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
ruby2_keywords(:method_missing)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module AbstractController
|
module AbstractController
|
||||||
class Error < StandardError #:nodoc:
|
class Error < StandardError # :nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "active_support/dependencies"
|
require "active_support/dependencies"
|
||||||
|
require "active_support/core_ext/name_error"
|
||||||
|
|
||||||
module AbstractController
|
module AbstractController
|
||||||
module Helpers
|
module Helpers
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
require "active_support/benchmarkable"
|
require "active_support/benchmarkable"
|
||||||
|
|
||||||
module AbstractController
|
module AbstractController
|
||||||
module Logger #:nodoc:
|
module Logger # :nodoc:
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "active_support/core_ext/module/introspection"
|
||||||
|
|
||||||
module AbstractController
|
module AbstractController
|
||||||
module Railties
|
module Railties
|
||||||
module RoutesHelpers
|
module RoutesHelpers
|
||||||
|
@ -18,10 +18,6 @@ module ActionController
|
|||||||
end
|
end
|
||||||
|
|
||||||
autoload_under "metal" do
|
autoload_under "metal" do
|
||||||
eager_autoload do
|
|
||||||
autoload :Live
|
|
||||||
end
|
|
||||||
|
|
||||||
autoload :ConditionalGet
|
autoload :ConditionalGet
|
||||||
autoload :ContentSecurityPolicy
|
autoload :ContentSecurityPolicy
|
||||||
autoload :Cookies
|
autoload :Cookies
|
||||||
@ -37,9 +33,11 @@ module ActionController
|
|||||||
autoload :BasicImplicitRender
|
autoload :BasicImplicitRender
|
||||||
autoload :ImplicitRender
|
autoload :ImplicitRender
|
||||||
autoload :Instrumentation
|
autoload :Instrumentation
|
||||||
|
autoload :Live
|
||||||
autoload :Logging
|
autoload :Logging
|
||||||
autoload :MimeResponds
|
autoload :MimeResponds
|
||||||
autoload :ParamsWrapper
|
autoload :ParamsWrapper
|
||||||
|
autoload :QueryTags
|
||||||
autoload :Redirecting
|
autoload :Redirecting
|
||||||
autoload :Renderers
|
autoload :Renderers
|
||||||
autoload :Rendering
|
autoload :Rendering
|
||||||
|
@ -37,7 +37,7 @@ module ActionController
|
|||||||
# == Renders
|
# == Renders
|
||||||
#
|
#
|
||||||
# The default API Controller stack includes all renderers, which means you
|
# The default API Controller stack includes all renderers, which means you
|
||||||
# can use <tt>render :json</tt> and brothers freely in your controllers. Keep
|
# can use <tt>render :json</tt> and siblings freely in your controllers. Keep
|
||||||
# in mind that templates are not going to be rendered, so you need to ensure
|
# in mind that templates are not going to be rendered, so you need to ensure
|
||||||
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
|
# your controller is calling either <tt>render</tt> or <tt>redirect_to</tt> in
|
||||||
# all actions, otherwise it will return 204 No Content.
|
# all actions, otherwise it will return 204 No Content.
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
require "active_support/core_ext/array/extract_options"
|
require "active_support/core_ext/array/extract_options"
|
||||||
require "action_dispatch/middleware/stack"
|
require "action_dispatch/middleware/stack"
|
||||||
require "action_dispatch/http/request"
|
|
||||||
require "action_dispatch/http/response"
|
|
||||||
|
|
||||||
module ActionController
|
module ActionController
|
||||||
# Extend ActionDispatch middleware stack to make it aware of options
|
# Extend ActionDispatch middleware stack to make it aware of options
|
||||||
@ -13,8 +11,8 @@ module ActionController
|
|||||||
# use AuthenticationMiddleware, except: [:index, :show]
|
# use AuthenticationMiddleware, except: [:index, :show]
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
|
class MiddlewareStack < ActionDispatch::MiddlewareStack # :nodoc:
|
||||||
class Middleware < ActionDispatch::MiddlewareStack::Middleware #:nodoc:
|
class Middleware < ActionDispatch::MiddlewareStack::Middleware # :nodoc:
|
||||||
def initialize(klass, args, actions, strategy, block)
|
def initialize(klass, args, actions, strategy, block)
|
||||||
@actions = actions
|
@actions = actions
|
||||||
@strategy = strategy
|
@strategy = strategy
|
||||||
@ -184,7 +182,7 @@ def performed?
|
|||||||
response_body || response.committed?
|
response_body || response.committed?
|
||||||
end
|
end
|
||||||
|
|
||||||
def dispatch(name, request, response) #:nodoc:
|
def dispatch(name, request, response) # :nodoc:
|
||||||
set_request!(request)
|
set_request!(request)
|
||||||
set_response!(response)
|
set_response!(response)
|
||||||
process(name)
|
process(name)
|
||||||
@ -196,12 +194,12 @@ def set_response!(response) # :nodoc:
|
|||||||
@_response = response
|
@_response = response
|
||||||
end
|
end
|
||||||
|
|
||||||
def set_request!(request) #:nodoc:
|
def set_request!(request) # :nodoc:
|
||||||
@_request = request
|
@_request = request
|
||||||
@_request.controller_instance = self
|
@_request.controller_instance = self
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_a #:nodoc:
|
def to_a # :nodoc:
|
||||||
response.to_a
|
response.to_a
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -115,6 +115,7 @@ def etag(&etagger)
|
|||||||
# before_action { fresh_when @article, template: 'widgets/show' }
|
# before_action { fresh_when @article, template: 'widgets/show' }
|
||||||
#
|
#
|
||||||
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil)
|
def fresh_when(object = nil, etag: nil, weak_etag: nil, strong_etag: nil, last_modified: nil, public: false, cache_control: {}, template: nil)
|
||||||
|
response.cache_control.delete(:no_store)
|
||||||
weak_etag ||= etag || object unless strong_etag
|
weak_etag ||= etag || object unless strong_etag
|
||||||
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
|
last_modified ||= object.try(:updated_at) || object.try(:maximum, :updated_at)
|
||||||
|
|
||||||
@ -273,6 +274,7 @@ def stale?(object = nil, **freshness_kwargs)
|
|||||||
#
|
#
|
||||||
# The method will also ensure an HTTP Date header for client compatibility.
|
# The method will also ensure an HTTP Date header for client compatibility.
|
||||||
def expires_in(seconds, options = {})
|
def expires_in(seconds, options = {})
|
||||||
|
response.cache_control.delete(:no_store)
|
||||||
response.cache_control.merge!(
|
response.cache_control.merge!(
|
||||||
max_age: seconds,
|
max_age: seconds,
|
||||||
public: options.delete(:public),
|
public: options.delete(:public),
|
||||||
@ -309,6 +311,12 @@ def http_cache_forever(public: false)
|
|||||||
public: public)
|
public: public)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Sets an HTTP 1.1 Cache-Control header of <tt>no-store</tt>. This means the
|
||||||
|
# resource may not be stored in any cache.
|
||||||
|
def no_store
|
||||||
|
response.cache_control.replace(no_store: true)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def combine_etags(validator, options)
|
def combine_etags(validator, options)
|
||||||
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
|
[validator, *etaggers.map { |etagger| instance_exec(options, &etagger) }].compact
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
module ContentSecurityPolicy
|
module ContentSecurityPolicy
|
||||||
# TODO: Documentation
|
# TODO: Documentation
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
module Cookies
|
module Cookies
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
require "action_controller/metal/exceptions"
|
require "action_controller/metal/exceptions"
|
||||||
require "action_dispatch/http/content_disposition"
|
require "action_dispatch/http/content_disposition"
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
# Methods for sending arbitrary data and for streaming files to the browser,
|
# Methods for sending arbitrary data and for streaming files to the browser,
|
||||||
# instead of rendering.
|
# instead of rendering.
|
||||||
module DataStreaming
|
module DataStreaming
|
||||||
@ -11,8 +11,8 @@ module DataStreaming
|
|||||||
|
|
||||||
include ActionController::Rendering
|
include ActionController::Rendering
|
||||||
|
|
||||||
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" #:nodoc:
|
DEFAULT_SEND_FILE_TYPE = "application/octet-stream" # :nodoc:
|
||||||
DEFAULT_SEND_FILE_DISPOSITION = "attachment" #:nodoc:
|
DEFAULT_SEND_FILE_DISPOSITION = "attachment" # :nodoc:
|
||||||
|
|
||||||
private
|
private
|
||||||
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
|
# Sends the file. This uses a server-appropriate method (such as X-Sendfile)
|
||||||
@ -66,7 +66,7 @@ module DataStreaming
|
|||||||
# https://www.mnot.net/cache_docs/ for an overview of web caching and
|
# https://www.mnot.net/cache_docs/ for an overview of web caching and
|
||||||
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
# https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
||||||
# for the Cache-Control header spec.
|
# for the Cache-Control header spec.
|
||||||
def send_file(path, options = {}) #:doc:
|
def send_file(path, options = {}) # :doc:
|
||||||
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
|
raise MissingFile, "Cannot read file #{path}" unless File.file?(path) && File.readable?(path)
|
||||||
|
|
||||||
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
|
options[:filename] ||= File.basename(path) unless options[:url_based_filename]
|
||||||
@ -106,7 +106,7 @@ def send_file(path, options = {}) #:doc:
|
|||||||
# send_data image.data, type: image.content_type, disposition: 'inline'
|
# send_data image.data, type: image.content_type, disposition: 'inline'
|
||||||
#
|
#
|
||||||
# See +send_file+ for more information on HTTP Content-* headers and caching.
|
# See +send_file+ for more information on HTTP Content-* headers and caching.
|
||||||
def send_data(data, options = {}) #:doc:
|
def send_data(data, options = {}) # :doc:
|
||||||
send_file_headers! options
|
send_file_headers! options
|
||||||
render options.slice(:status, :content_type).merge(body: data)
|
render options.slice(:status, :content_type).merge(body: data)
|
||||||
end
|
end
|
||||||
@ -138,14 +138,6 @@ def send_file_headers!(options)
|
|||||||
end
|
end
|
||||||
|
|
||||||
headers["Content-Transfer-Encoding"] = "binary"
|
headers["Content-Transfer-Encoding"] = "binary"
|
||||||
|
|
||||||
# Fix a problem with IE 6.0 on opening downloaded files:
|
|
||||||
# If Cache-Control: no-cache is set (which Rails does by default),
|
|
||||||
# IE removes the file it just downloaded from its cache immediately
|
|
||||||
# after it displays the "open/save" dialog, which means that if you
|
|
||||||
# hit "open" the file isn't there anymore when the application that
|
|
||||||
# is called for handling the download is run, so let's workaround that
|
|
||||||
response.cache_control[:public] ||= false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -44,7 +44,7 @@ def determine_template_etag(options)
|
|||||||
# template digest from the ETag.
|
# template digest from the ETag.
|
||||||
def pick_template_for_etag(options)
|
def pick_template_for_etag(options)
|
||||||
unless options[:template] == false
|
unless options[:template] == false
|
||||||
options[:template] || "#{controller_path}/#{action_name}"
|
options[:template] || lookup_context.find_all(action_name, _prefixes).first&.virtual_path
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController
|
module ActionController
|
||||||
class ActionControllerError < StandardError #:nodoc:
|
class ActionControllerError < StandardError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class BadRequest < ActionControllerError #:nodoc:
|
class BadRequest < ActionControllerError # :nodoc:
|
||||||
def initialize(msg = nil)
|
def initialize(msg = nil)
|
||||||
super(msg)
|
super(msg)
|
||||||
set_backtrace $!.backtrace if $!
|
set_backtrace $!.backtrace if $!
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class RenderError < ActionControllerError #:nodoc:
|
class RenderError < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class RoutingError < ActionControllerError #:nodoc:
|
class RoutingError < ActionControllerError # :nodoc:
|
||||||
attr_reader :failures
|
attr_reader :failures
|
||||||
def initialize(message, failures = [])
|
def initialize(message, failures = [])
|
||||||
super(message)
|
super(message)
|
||||||
@ -22,7 +22,7 @@ def initialize(message, failures = [])
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class UrlGenerationError < ActionControllerError #:nodoc:
|
class UrlGenerationError < ActionControllerError # :nodoc:
|
||||||
attr_reader :routes, :route_name, :method_name
|
attr_reader :routes, :route_name, :method_name
|
||||||
|
|
||||||
def initialize(message, routes = nil, route_name = nil, method_name = nil)
|
def initialize(message, routes = nil, route_name = nil, method_name = nil)
|
||||||
@ -33,44 +33,33 @@ def initialize(message, routes = nil, route_name = nil, method_name = nil)
|
|||||||
super(message)
|
super(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
class Correction
|
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
|
||||||
def initialize(error)
|
include DidYouMean::Correctable
|
||||||
@error = error
|
|
||||||
end
|
|
||||||
|
|
||||||
def corrections
|
def corrections
|
||||||
if @error.method_name
|
@corrections ||= begin
|
||||||
maybe_these = @error.routes.named_routes.helper_names.grep(/#{@error.route_name}/)
|
maybe_these = routes&.named_routes&.helper_names&.grep(/#{route_name}/) || []
|
||||||
maybe_these -= [@error.method_name.to_s] # remove exact match
|
maybe_these -= [method_name.to_s] # remove exact match
|
||||||
|
|
||||||
maybe_these.sort_by { |n|
|
DidYouMean::SpellChecker.new(dictionary: maybe_these).correct(route_name)
|
||||||
DidYouMean::Jaro.distance(@error.route_name, n)
|
end
|
||||||
}.reverse.first(4)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# We may not have DYM, and DYM might not let us register error handlers
|
class MethodNotAllowed < ActionControllerError # :nodoc:
|
||||||
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
|
|
||||||
DidYouMean.correct_error(self, Correction)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
class MethodNotAllowed < ActionControllerError #:nodoc:
|
|
||||||
def initialize(*allowed_methods)
|
def initialize(*allowed_methods)
|
||||||
super("Only #{allowed_methods.to_sentence} requests are allowed.")
|
super("Only #{allowed_methods.to_sentence} requests are allowed.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class NotImplemented < MethodNotAllowed #:nodoc:
|
class NotImplemented < MethodNotAllowed # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class MissingFile < ActionControllerError #:nodoc:
|
class MissingFile < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class SessionOverflowError < ActionControllerError #:nodoc:
|
class SessionOverflowError < ActionControllerError # :nodoc:
|
||||||
DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
|
DEFAULT_MESSAGE = "Your session data is larger than the data column in which it is to be stored. You must increase the size of your data column if you intend to store large data."
|
||||||
|
|
||||||
def initialize(message = nil)
|
def initialize(message = nil)
|
||||||
@ -78,10 +67,10 @@ def initialize(message = nil)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class UnknownHttpMethod < ActionControllerError #:nodoc:
|
class UnknownHttpMethod < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class UnknownFormat < ActionControllerError #:nodoc:
|
class UnknownFormat < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
# Raised when a nested respond_to is triggered and the content types of each
|
# Raised when a nested respond_to is triggered and the content types of each
|
||||||
@ -102,6 +91,6 @@ def initialize(message = nil)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class MissingExactTemplate < UnknownFormat #:nodoc:
|
class MissingExactTemplate < UnknownFormat # :nodoc:
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
module Flash
|
module Flash
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
@ -41,10 +41,14 @@ def add_flash_types(*types)
|
|||||||
self._flash_types += [type]
|
self._flash_types += [type]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def action_methods # :nodoc:
|
||||||
|
@action_methods ||= super - _flash_types.map(&:to_s).to_set
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
def redirect_to(options = {}, response_options_and_flash = {}) #:doc:
|
def redirect_to(options = {}, response_options_and_flash = {}) # :doc:
|
||||||
self.class._flash_types.each do |flash_type|
|
self.class._flash_types.each do |flash_type|
|
||||||
if type = response_options_and_flash.delete(flash_type)
|
if type = response_options_and_flash.delete(flash_type)
|
||||||
flash[flash_type] = type
|
flash[flash_type] = type
|
||||||
|
@ -138,11 +138,11 @@ def authentication_request(controller, realm, message)
|
|||||||
#
|
#
|
||||||
# === Simple \Digest example
|
# === Simple \Digest example
|
||||||
#
|
#
|
||||||
# require "digest/md5"
|
# require "openssl"
|
||||||
# class PostsController < ApplicationController
|
# class PostsController < ApplicationController
|
||||||
# REALM = "SuperSecret"
|
# REALM = "SuperSecret"
|
||||||
# USERS = {"dhh" => "secret", #plain text password
|
# USERS = {"dhh" => "secret", #plain text password
|
||||||
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
|
# "dap" => OpenSSL::Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
|
||||||
#
|
#
|
||||||
# before_action :authenticate, except: [:index]
|
# before_action :authenticate, except: [:index]
|
||||||
#
|
#
|
||||||
@ -230,12 +230,12 @@ def validate_digest_response(request, realm, &password_procedure)
|
|||||||
# of a plain-text password.
|
# of a plain-text password.
|
||||||
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
|
def expected_response(http_method, uri, credentials, password, password_is_ha1 = true)
|
||||||
ha1 = password_is_ha1 ? password : ha1(credentials, password)
|
ha1 = password_is_ha1 ? password : ha1(credentials, password)
|
||||||
ha2 = ::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
|
ha2 = OpenSSL::Digest::MD5.hexdigest([http_method.to_s.upcase, uri].join(":"))
|
||||||
::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
|
OpenSSL::Digest::MD5.hexdigest([ha1, credentials[:nonce], credentials[:nc], credentials[:cnonce], credentials[:qop], ha2].join(":"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def ha1(credentials, password)
|
def ha1(credentials, password)
|
||||||
::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
|
OpenSSL::Digest::MD5.hexdigest([credentials[:username], credentials[:realm], password].join(":"))
|
||||||
end
|
end
|
||||||
|
|
||||||
def encode_credentials(http_method, credentials, password, password_is_ha1)
|
def encode_credentials(http_method, credentials, password, password_is_ha1)
|
||||||
@ -309,7 +309,7 @@ def secret_token(request)
|
|||||||
def nonce(secret_key, time = Time.now)
|
def nonce(secret_key, time = Time.now)
|
||||||
t = time.to_i
|
t = time.to_i
|
||||||
hashed = [t, secret_key]
|
hashed = [t, secret_key]
|
||||||
digest = ::Digest::MD5.hexdigest(hashed.join(":"))
|
digest = OpenSSL::Digest::MD5.hexdigest(hashed.join(":"))
|
||||||
::Base64.strict_encode64("#{t}:#{digest}")
|
::Base64.strict_encode64("#{t}:#{digest}")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -326,7 +326,7 @@ def validate_nonce(secret_key, request, value, seconds_to_timeout = 5 * 60)
|
|||||||
|
|
||||||
# Opaque based on digest of secret key
|
# Opaque based on digest of secret key
|
||||||
def opaque(secret_key)
|
def opaque(secret_key)
|
||||||
::Digest::MD5.hexdigest(secret_key)
|
OpenSSL::Digest::MD5.hexdigest(secret_key)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -99,7 +99,7 @@ module ClassMethods
|
|||||||
# A hook which allows other frameworks to log what happened during
|
# A hook which allows other frameworks to log what happened during
|
||||||
# controller process action. This method should return an array
|
# controller process action. This method should return an array
|
||||||
# with the messages to be added.
|
# with the messages to be added.
|
||||||
def log_process_action(payload) #:nodoc:
|
def log_process_action(payload) # :nodoc:
|
||||||
messages, view_runtime = [], payload[:view_runtime]
|
messages, view_runtime = [], payload[:view_runtime]
|
||||||
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
|
messages << ("Views: %.1fms" % view_runtime.to_f) if view_runtime
|
||||||
messages
|
messages
|
||||||
|
@ -124,9 +124,14 @@ def perform_write(json, options)
|
|||||||
class ClientDisconnected < RuntimeError
|
class ClientDisconnected < RuntimeError
|
||||||
end
|
end
|
||||||
|
|
||||||
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
|
class Buffer < ActionDispatch::Response::Buffer # :nodoc:
|
||||||
include MonitorMixin
|
include MonitorMixin
|
||||||
|
|
||||||
|
class << self
|
||||||
|
attr_accessor :queue_size
|
||||||
|
end
|
||||||
|
@queue_size = 10
|
||||||
|
|
||||||
# Ignore that the client has disconnected.
|
# Ignore that the client has disconnected.
|
||||||
#
|
#
|
||||||
# If this value is `true`, calling `write` after the client
|
# If this value is `true`, calling `write` after the client
|
||||||
@ -136,7 +141,7 @@ class Buffer < ActionDispatch::Response::Buffer #:nodoc:
|
|||||||
attr_accessor :ignore_disconnect
|
attr_accessor :ignore_disconnect
|
||||||
|
|
||||||
def initialize(response)
|
def initialize(response)
|
||||||
super(response, SizedQueue.new(10))
|
super(response, build_queue(self.class.queue_size))
|
||||||
@error_callback = lambda { true }
|
@error_callback = lambda { true }
|
||||||
@cv = new_cond
|
@cv = new_cond
|
||||||
@aborted = false
|
@aborted = false
|
||||||
@ -219,9 +224,13 @@ def each_chunk(&block)
|
|||||||
yield str
|
yield str
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build_queue(queue_size)
|
||||||
|
queue_size ? SizedQueue.new(queue_size) : Queue.new
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class Response < ActionDispatch::Response #:nodoc: all
|
class Response < ActionDispatch::Response # :nodoc: all
|
||||||
private
|
private
|
||||||
def before_committed
|
def before_committed
|
||||||
super
|
super
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require "abstract_controller/collector"
|
require "abstract_controller/collector"
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
module MimeResponds
|
module MimeResponds
|
||||||
# Without web-service support, an action which collects the data for displaying a list of people
|
# Without web-service support, an action which collects the data for displaying a list of people
|
||||||
# might look something like this:
|
# might look something like this:
|
||||||
@ -289,7 +289,7 @@ def negotiate_format(request)
|
|||||||
@format = request.negotiate_mime(@responses.keys)
|
@format = request.negotiate_mime(@responses.keys)
|
||||||
end
|
end
|
||||||
|
|
||||||
class VariantCollector #:nodoc:
|
class VariantCollector # :nodoc:
|
||||||
def initialize(variant = nil)
|
def initialize(variant = nil)
|
||||||
@variant = variant
|
@variant = variant
|
||||||
@variants = {}
|
@variants = {}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
# HTTP Permissions Policy is a web standard for defining a mechanism to
|
# HTTP Permissions Policy is a web standard for defining a mechanism to
|
||||||
# allow and deny the use of browser permissions in its own context, and
|
# allow and deny the use of browser permissions in its own context, and
|
||||||
# in content within any <iframe> elements in the document.
|
# in content within any <iframe> elements in the document.
|
||||||
|
16
actionpack/lib/action_controller/metal/query_tags.rb
Normal file
16
actionpack/lib/action_controller/metal/query_tags.rb
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActionController
|
||||||
|
module QueryTags # :nodoc:
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
around_action :expose_controller_to_query_logs
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def expose_controller_to_query_logs(&block)
|
||||||
|
ActiveRecord::QueryLogs.set_context(controller: self, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -7,6 +7,10 @@ module Redirecting
|
|||||||
include AbstractController::Logger
|
include AbstractController::Logger
|
||||||
include ActionController::UrlFor
|
include ActionController::UrlFor
|
||||||
|
|
||||||
|
included do
|
||||||
|
mattr_accessor :raise_on_open_redirects, default: false
|
||||||
|
end
|
||||||
|
|
||||||
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
|
# Redirects the browser to the target specified in +options+. This parameter can be any one of:
|
||||||
#
|
#
|
||||||
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
|
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
|
||||||
@ -57,20 +61,22 @@ module Redirecting
|
|||||||
#
|
#
|
||||||
# redirect_to post_url(@post) and return
|
# redirect_to post_url(@post) and return
|
||||||
#
|
#
|
||||||
# Passing user input directly into +redirect_to+ is considered dangerous (eg. `redirect_to(params[:location])`).
|
# Passing user input directly into +redirect_to+ is considered dangerous (e.g. `redirect_to(params[:location])`).
|
||||||
# Always use regular expressions or a permitted list when redirecting to a user specified location.
|
# Always use regular expressions or a permitted list when redirecting to a user specified location.
|
||||||
def redirect_to(options = {}, response_options = {})
|
def redirect_to(options = {}, response_options = {})
|
||||||
|
response_options[:allow_other_host] ||= _allow_other_host unless response_options.key?(:allow_other_host)
|
||||||
|
|
||||||
raise ActionControllerError.new("Cannot redirect to nil!") unless options
|
raise ActionControllerError.new("Cannot redirect to nil!") unless options
|
||||||
raise AbstractController::DoubleRenderError if response_body
|
raise AbstractController::DoubleRenderError if response_body
|
||||||
|
|
||||||
self.status = _extract_redirect_to_status(options, response_options)
|
self.status = _extract_redirect_to_status(options, response_options)
|
||||||
self.location = _compute_redirect_to_location(request, options)
|
self.location = _compute_safe_redirect_to_location(request, options, response_options)
|
||||||
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
|
self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(response.location)}\">redirected</a>.</body></html>"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
|
# Soft deprecated alias for <tt>redirect_back_or_to</tt> where the fallback_location location is supplied as a keyword argument instead
|
||||||
# of the first positional argument.
|
# of the first positional argument.
|
||||||
def redirect_back(fallback_location:, allow_other_host: true, **args)
|
def redirect_back(fallback_location:, allow_other_host: _allow_other_host, **args)
|
||||||
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
|
redirect_back_or_to fallback_location, allow_other_host: allow_other_host, **args
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -96,13 +102,28 @@ def redirect_back(fallback_location:, allow_other_host: true, **args)
|
|||||||
#
|
#
|
||||||
# All other options that can be passed to #redirect_to are accepted as
|
# All other options that can be passed to #redirect_to are accepted as
|
||||||
# options and the behavior is identical.
|
# options and the behavior is identical.
|
||||||
def redirect_back_or_to(fallback_location, allow_other_host: true, **args)
|
def redirect_back_or_to(fallback_location, allow_other_host: _allow_other_host, **options)
|
||||||
referer = request.headers["Referer"]
|
location = request.referer || fallback_location
|
||||||
redirect_to_referer = referer && (allow_other_host || _url_host_allowed?(referer))
|
location = fallback_location unless allow_other_host || _url_host_allowed?(request.referer)
|
||||||
redirect_to redirect_to_referer ? referer : fallback_location, **args
|
allow_other_host = true if _allow_other_host && !allow_other_host # if the fallback is an open redirect
|
||||||
|
|
||||||
|
redirect_to location, allow_other_host: allow_other_host, **options
|
||||||
end
|
end
|
||||||
|
|
||||||
def _compute_redirect_to_location(request, options) #:nodoc:
|
def _compute_safe_redirect_to_location(request, options, response_options)
|
||||||
|
location = _compute_redirect_to_location(request, options)
|
||||||
|
|
||||||
|
if response_options[:allow_other_host] || _url_host_allowed?(location)
|
||||||
|
location
|
||||||
|
else
|
||||||
|
raise(ArgumentError, <<~MSG.squish)
|
||||||
|
Unsafe redirect #{location.truncate(100).inspect},
|
||||||
|
use :allow_other_host to redirect anyway.
|
||||||
|
MSG
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def _compute_redirect_to_location(request, options) # :nodoc:
|
||||||
case options
|
case options
|
||||||
# The scheme name consist of a letter followed by any combination of
|
# The scheme name consist of a letter followed by any combination of
|
||||||
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
|
# letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
|
||||||
@ -123,6 +144,10 @@ def _compute_redirect_to_location(request, options) #:nodoc:
|
|||||||
public :_compute_redirect_to_location
|
public :_compute_redirect_to_location
|
||||||
|
|
||||||
private
|
private
|
||||||
|
def _allow_other_host
|
||||||
|
!raise_on_open_redirects
|
||||||
|
end
|
||||||
|
|
||||||
def _extract_redirect_to_status(options, response_options)
|
def _extract_redirect_to_status(options, response_options)
|
||||||
if options.is_a?(Hash) && options.key?(:status)
|
if options.is_a?(Hash) && options.key?(:status)
|
||||||
Rack::Utils.status_code(options.delete(:status))
|
Rack::Utils.status_code(options.delete(:status))
|
||||||
|
@ -25,7 +25,7 @@ def inherited(klass)
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Check for double render errors and set the content_type after rendering.
|
# Check for double render errors and set the content_type after rendering.
|
||||||
def render(*args) #:nodoc:
|
def render(*args) # :nodoc:
|
||||||
raise ::AbstractController::DoubleRenderError if response_body
|
raise ::AbstractController::DoubleRenderError if response_body
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
@ -48,7 +48,7 @@ def render_to_body(options = {})
|
|||||||
|
|
||||||
private
|
private
|
||||||
# Before processing, set the request formats in current controller formats.
|
# Before processing, set the request formats in current controller formats.
|
||||||
def process_action(*) #:nodoc:
|
def process_action(*) # :nodoc:
|
||||||
self.formats = request.formats.filter_map(&:ref)
|
self.formats = request.formats.filter_map(&:ref)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
require "action_controller/metal/exceptions"
|
require "action_controller/metal/exceptions"
|
||||||
require "active_support/security_utils"
|
require "active_support/security_utils"
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
|
class InvalidAuthenticityToken < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
|
class InvalidCrossOriginRequest < ActionControllerError # :nodoc:
|
||||||
end
|
end
|
||||||
|
|
||||||
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
|
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
|
||||||
@ -186,7 +186,7 @@ def handle_unverified_request
|
|||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
|
class NullSessionHash < Rack::Session::Abstract::SessionHash # :nodoc:
|
||||||
def initialize(req)
|
def initialize(req)
|
||||||
super(nil, req)
|
super(nil, req)
|
||||||
@data = {}
|
@data = {}
|
||||||
@ -205,7 +205,7 @@ def enabled?
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
|
class NullCookieJar < ActionDispatch::Cookies::CookieJar # :nodoc:
|
||||||
def write(*)
|
def write(*)
|
||||||
# nothing
|
# nothing
|
||||||
end
|
end
|
||||||
@ -223,12 +223,14 @@ def handle_unverified_request
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Exception
|
class Exception
|
||||||
|
attr_accessor :warning_message
|
||||||
|
|
||||||
def initialize(controller)
|
def initialize(controller)
|
||||||
@controller = controller
|
@controller = controller
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_unverified_request
|
def handle_unverified_request
|
||||||
raise ActionController::InvalidAuthenticityToken
|
raise ActionController::InvalidAuthenticityToken, warning_message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@ -248,22 +250,31 @@ def verify_authenticity_token # :doc:
|
|||||||
mark_for_same_origin_verification!
|
mark_for_same_origin_verification!
|
||||||
|
|
||||||
if !verified_request?
|
if !verified_request?
|
||||||
if logger && log_warning_on_csrf_failure
|
logger.warn unverified_request_warning_message if logger && log_warning_on_csrf_failure
|
||||||
if valid_request_origin?
|
|
||||||
logger.warn "Can't verify CSRF token authenticity."
|
|
||||||
else
|
|
||||||
logger.warn "HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
handle_unverified_request
|
handle_unverified_request
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_unverified_request # :doc:
|
def handle_unverified_request # :doc:
|
||||||
forgery_protection_strategy.new(self).handle_unverified_request
|
protection_strategy = forgery_protection_strategy.new(self)
|
||||||
|
|
||||||
|
if protection_strategy.respond_to?(:warning_message)
|
||||||
|
protection_strategy.warning_message = unverified_request_warning_message
|
||||||
end
|
end
|
||||||
|
|
||||||
#:nodoc:
|
protection_strategy.handle_unverified_request
|
||||||
|
end
|
||||||
|
|
||||||
|
def unverified_request_warning_message # :nodoc:
|
||||||
|
if valid_request_origin?
|
||||||
|
"Can't verify CSRF token authenticity."
|
||||||
|
else
|
||||||
|
"HTTP Origin header (#{request.origin}) didn't match request.base_url (#{request.base_url})"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# :nodoc:
|
||||||
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
|
CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
|
||||||
"<script> tag on another site requested protected JavaScript. " \
|
"<script> tag on another site requested protected JavaScript. " \
|
||||||
"If you know what you're doing, go ahead and disable forgery " \
|
"If you know what you're doing, go ahead and disable forgery " \
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
# This module is responsible for providing +rescue_from+ helpers
|
# This module is responsible for providing +rescue_from+ helpers
|
||||||
# to controllers and configuring when detailed exceptions must be
|
# to controllers and configuring when detailed exceptions must be
|
||||||
# shown.
|
# shown.
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require "rack/chunked"
|
require "rack/chunked"
|
||||||
|
|
||||||
module ActionController #:nodoc:
|
module ActionController # :nodoc:
|
||||||
# Allows views to be streamed back to the client as they are rendered.
|
# Allows views to be streamed back to the client as they are rendered.
|
||||||
#
|
#
|
||||||
# By default, Rails renders views by first rendering the template
|
# By default, Rails renders views by first rendering the template
|
||||||
|
@ -27,30 +27,15 @@ def initialize(param, keys = nil) # :nodoc:
|
|||||||
super("param is missing or the value is empty: #{param}")
|
super("param is missing or the value is empty: #{param}")
|
||||||
end
|
end
|
||||||
|
|
||||||
class Correction
|
if defined?(DidYouMean::Correctable) && defined?(DidYouMean::SpellChecker)
|
||||||
def initialize(error)
|
include DidYouMean::Correctable # :nodoc:
|
||||||
@error = error
|
|
||||||
end
|
|
||||||
|
|
||||||
def corrections
|
def corrections # :nodoc:
|
||||||
if @error.param && @error.keys
|
@corrections ||= DidYouMean::SpellChecker.new(dictionary: keys).correct(param.to_s)
|
||||||
maybe_these = @error.keys
|
|
||||||
|
|
||||||
maybe_these.sort_by { |n|
|
|
||||||
DidYouMean::Jaro.distance(@error.param.to_s, n)
|
|
||||||
}.reverse.first(4)
|
|
||||||
else
|
|
||||||
[]
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# We may not have DYM, and DYM might not let us register error handlers
|
|
||||||
if defined?(DidYouMean) && DidYouMean.respond_to?(:correct_error)
|
|
||||||
DidYouMean.correct_error(self, Correction)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Raised when a supplied parameter is not expected and
|
# Raised when a supplied parameter is not expected and
|
||||||
# ActionController::Parameters.action_on_unpermitted_parameters
|
# ActionController::Parameters.action_on_unpermitted_parameters
|
||||||
# is set to <tt>:raise</tt>.
|
# is set to <tt>:raise</tt>.
|
||||||
@ -955,7 +940,7 @@ def convert_value_to_parameters(value)
|
|||||||
def each_element(object, &block)
|
def each_element(object, &block)
|
||||||
case object
|
case object
|
||||||
when Array
|
when Array
|
||||||
object.grep(Parameters).filter_map { |el| yield el }
|
object.grep(Parameters).filter_map(&block)
|
||||||
when Parameters
|
when Parameters
|
||||||
if object.nested_attributes?
|
if object.nested_attributes?
|
||||||
object.each_nested_attribute(&block)
|
object.each_nested_attribute(&block)
|
||||||
|
@ -8,8 +8,10 @@
|
|||||||
require "action_view/railtie"
|
require "action_view/railtie"
|
||||||
|
|
||||||
module ActionController
|
module ActionController
|
||||||
class Railtie < Rails::Railtie #:nodoc:
|
class Railtie < Rails::Railtie # :nodoc:
|
||||||
config.action_controller = ActiveSupport::OrderedOptions.new
|
config.action_controller = ActiveSupport::OrderedOptions.new
|
||||||
|
config.action_controller.raise_on_open_redirects = false
|
||||||
|
config.action_controller.log_query_tags_around_actions = true
|
||||||
|
|
||||||
config.eager_load_namespaces << ActionController
|
config.eager_load_namespaces << ActionController
|
||||||
|
|
||||||
@ -25,14 +27,19 @@ class Railtie < Rails::Railtie #:nodoc:
|
|||||||
options = app.config.action_controller
|
options = app.config.action_controller
|
||||||
|
|
||||||
ActiveSupport.on_load(:action_controller, run_once: true) do
|
ActiveSupport.on_load(:action_controller, run_once: true) do
|
||||||
ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
|
ActionController::Parameters.permit_all_parameters = options.permit_all_parameters || false
|
||||||
if app.config.action_controller[:always_permitted_parameters]
|
if app.config.action_controller[:always_permitted_parameters]
|
||||||
ActionController::Parameters.always_permitted_parameters =
|
ActionController::Parameters.always_permitted_parameters =
|
||||||
app.config.action_controller.delete(:always_permitted_parameters)
|
app.config.action_controller.always_permitted_parameters
|
||||||
end
|
end
|
||||||
ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
|
|
||||||
(Rails.env.test? || Rails.env.development?) ? :log : false
|
action_on_unpermitted_parameters = options.action_on_unpermitted_parameters
|
||||||
|
|
||||||
|
if action_on_unpermitted_parameters.nil?
|
||||||
|
action_on_unpermitted_parameters = (Rails.env.test? || Rails.env.development?) ? :log : false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
ActionController::Parameters.action_on_unpermitted_parameters = action_on_unpermitted_parameters
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -55,6 +62,14 @@ class Railtie < Rails::Railtie #:nodoc:
|
|||||||
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
|
extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
|
||||||
extend ::ActionController::Railties::Helpers
|
extend ::ActionController::Railties::Helpers
|
||||||
|
|
||||||
|
# Configs used in other initializers
|
||||||
|
options = options.except(
|
||||||
|
:log_query_tags_around_actions,
|
||||||
|
:permit_all_parameters,
|
||||||
|
:action_on_unpermitted_parameters,
|
||||||
|
:always_permitted_parameters
|
||||||
|
)
|
||||||
|
|
||||||
options.each do |k, v|
|
options.each do |k, v|
|
||||||
k = "#{k}="
|
k = "#{k}="
|
||||||
if respond_to?(k)
|
if respond_to?(k)
|
||||||
@ -85,5 +100,27 @@ class Railtie < Rails::Railtie #:nodoc:
|
|||||||
ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
|
ActionController::Metal.descendants.each(&:action_methods) if config.eager_load
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
initializer "action_controller.query_log_tags" do |app|
|
||||||
|
query_logs_tags_enabled = app.config.respond_to?(:active_record) &&
|
||||||
|
app.config.active_record.query_log_tags_enabled &&
|
||||||
|
app.config.action_controller.log_query_tags_around_actions
|
||||||
|
|
||||||
|
if query_logs_tags_enabled
|
||||||
|
app.config.active_record.query_log_tags += [:controller, :action]
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:action_controller) do
|
||||||
|
include ActionController::QueryTags
|
||||||
|
end
|
||||||
|
|
||||||
|
ActiveSupport.on_load(:active_record) do
|
||||||
|
ActiveRecord::QueryLogs.taggings.merge!(
|
||||||
|
controller: ->(context) { context[:controller].controller_name },
|
||||||
|
action: ->(context) { context[:controller].action_name },
|
||||||
|
namespaced_controller: ->(context) { context[:controller].class.name }
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -24,11 +24,14 @@ module Live
|
|||||||
def new_controller_thread # :nodoc:
|
def new_controller_thread # :nodoc:
|
||||||
yield
|
yield
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Avoid a deadlock from the queue filling up
|
||||||
|
Buffer.queue_size = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
# ActionController::TestCase will be deprecated and moved to a gem in the future.
|
# ActionController::TestCase will be deprecated and moved to a gem in the future.
|
||||||
# Please use ActionDispatch::IntegrationTest going forward.
|
# Please use ActionDispatch::IntegrationTest going forward.
|
||||||
class TestRequest < ActionDispatch::TestRequest #:nodoc:
|
class TestRequest < ActionDispatch::TestRequest # :nodoc:
|
||||||
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
|
DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup
|
||||||
DEFAULT_ENV.delete "PATH_INFO"
|
DEFAULT_ENV.delete "PATH_INFO"
|
||||||
|
|
||||||
@ -176,7 +179,7 @@ class LiveTestResponse < Live::Response
|
|||||||
|
|
||||||
# Methods #destroy and #load! are overridden to avoid calling methods on the
|
# Methods #destroy and #load! are overridden to avoid calling methods on the
|
||||||
# @store object, which does not exist for the TestSession class.
|
# @store object, which does not exist for the TestSession class.
|
||||||
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash #:nodoc:
|
class TestSession < Rack::Session::Abstract::PersistedSecure::SecureSessionHash # :nodoc:
|
||||||
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
|
DEFAULT_OPTIONS = Rack::Session::Abstract::Persisted::DEFAULT_OPTIONS
|
||||||
|
|
||||||
def initialize(session = {})
|
def initialize(session = {})
|
||||||
|
@ -187,6 +187,12 @@ def merge_and_normalize_cache_control!(cache_control)
|
|||||||
|
|
||||||
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
|
return if control.empty? && cache_control.empty? # Let middleware handle default behavior
|
||||||
|
|
||||||
|
if cache_control.any?
|
||||||
|
# Any caching directive coming from a controller overrides
|
||||||
|
# no-cache/no-store in the default Cache-Control header.
|
||||||
|
control.delete(:no_cache)
|
||||||
|
control.delete(:no_store)
|
||||||
|
|
||||||
if extras = control.delete(:extras)
|
if extras = control.delete(:extras)
|
||||||
cache_control[:extras] ||= []
|
cache_control[:extras] ||= []
|
||||||
cache_control[:extras] += extras
|
cache_control[:extras] += extras
|
||||||
@ -194,6 +200,7 @@ def merge_and_normalize_cache_control!(cache_control)
|
|||||||
end
|
end
|
||||||
|
|
||||||
control.merge! cache_control
|
control.merge! cache_control
|
||||||
|
end
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require "active_support/core_ext/object/deep_dup"
|
require "active_support/core_ext/object/deep_dup"
|
||||||
|
|
||||||
module ActionDispatch #:nodoc:
|
module ActionDispatch # :nodoc:
|
||||||
class ContentSecurityPolicy
|
class ContentSecurityPolicy
|
||||||
class Middleware
|
class Middleware
|
||||||
CONTENT_TYPE = "Content-Type"
|
CONTENT_TYPE = "Content-Type"
|
||||||
|
@ -13,8 +13,8 @@ def initialize
|
|||||||
@symbols = []
|
@symbols = []
|
||||||
end
|
end
|
||||||
|
|
||||||
def each
|
def each(&block)
|
||||||
@mimes.each { |x| yield x }
|
@mimes.each(&block)
|
||||||
end
|
end
|
||||||
|
|
||||||
def <<(type)
|
def <<(type)
|
||||||
@ -42,9 +42,9 @@ def [](type)
|
|||||||
Type.lookup_by_extension(type)
|
Type.lookup_by_extension(type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fetch(type)
|
def fetch(type, &block)
|
||||||
return type if type.is_a?(Type)
|
return type if type.is_a?(Type)
|
||||||
EXTENSION_LOOKUP.fetch(type.to_s) { |k| yield k }
|
EXTENSION_LOOKUP.fetch(type.to_s, &block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ class Type
|
|||||||
@register_callbacks = []
|
@register_callbacks = []
|
||||||
|
|
||||||
# A simple helper class used in parsing the accept header.
|
# A simple helper class used in parsing the accept header.
|
||||||
class AcceptItem #:nodoc:
|
class AcceptItem # :nodoc:
|
||||||
attr_accessor :index, :name, :q
|
attr_accessor :index, :name, :q
|
||||||
alias :to_s :name
|
alias :to_s :name
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ def <=>(item)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
class AcceptList #:nodoc:
|
class AcceptList # :nodoc:
|
||||||
def self.sort!(list)
|
def self.sort!(list)
|
||||||
list.sort!
|
list.sort!
|
||||||
|
|
||||||
|
@ -17,8 +17,8 @@ module Parameters
|
|||||||
# Raised when raw data from the request cannot be parsed by the parser
|
# Raised when raw data from the request cannot be parsed by the parser
|
||||||
# defined for request's content MIME type.
|
# defined for request's content MIME type.
|
||||||
class ParseError < StandardError
|
class ParseError < StandardError
|
||||||
def initialize
|
def initialize(message = $!.message)
|
||||||
super($!.message)
|
super(message)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ def parameters
|
|||||||
end
|
end
|
||||||
alias :params :parameters
|
alias :params :parameters
|
||||||
|
|
||||||
def path_parameters=(parameters) #:nodoc:
|
def path_parameters=(parameters) # :nodoc:
|
||||||
delete_header("action_dispatch.request.parameters")
|
delete_header("action_dispatch.request.parameters")
|
||||||
|
|
||||||
parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
|
parameters = Request::Utils.set_binary_encoding(self, parameters, parameters[:controller], parameters[:action])
|
||||||
@ -93,7 +93,7 @@ def parse_formatted_parameters(parsers)
|
|||||||
strategy.call(raw_post)
|
strategy.call(raw_post)
|
||||||
rescue # JSON or Ruby code block errors.
|
rescue # JSON or Ruby code block errors.
|
||||||
log_parse_error_once
|
log_parse_error_once
|
||||||
raise ParseError
|
raise ParseError, "Error occurred while parsing request parameters"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
require "active_support/core_ext/object/deep_dup"
|
require "active_support/core_ext/object/deep_dup"
|
||||||
|
|
||||||
module ActionDispatch #:nodoc:
|
module ActionDispatch # :nodoc:
|
||||||
class PermissionsPolicy
|
class PermissionsPolicy
|
||||||
class Middleware
|
class Middleware
|
||||||
CONTENT_TYPE = "Content-Type"
|
CONTENT_TYPE = "Content-Type"
|
||||||
|
@ -87,7 +87,7 @@ def controller_class_for(name)
|
|||||||
controller_param = name.underscore
|
controller_param = name.underscore
|
||||||
const_name = controller_param.camelize << "Controller"
|
const_name = controller_param.camelize << "Controller"
|
||||||
begin
|
begin
|
||||||
ActiveSupport::Dependencies.constantize(const_name)
|
const_name.constantize
|
||||||
rescue NameError => error
|
rescue NameError => error
|
||||||
if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
|
if error.missing_name == const_name || const_name.start_with?("#{error.missing_name}::")
|
||||||
raise MissingController.new(error.message, error.name)
|
raise MissingController.new(error.message, error.name)
|
||||||
@ -162,7 +162,7 @@ def engine_script_name=(name) # :nodoc:
|
|||||||
set_header(routes.env_key, name.dup)
|
set_header(routes.env_key, name.dup)
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_method=(request_method) #:nodoc:
|
def request_method=(request_method) # :nodoc:
|
||||||
if check_method(request_method)
|
if check_method(request_method)
|
||||||
@request_method = set_header("REQUEST_METHOD", request_method)
|
@request_method = set_header("REQUEST_METHOD", request_method)
|
||||||
end
|
end
|
||||||
@ -352,7 +352,7 @@ def form_data?
|
|||||||
FORM_DATA_MEDIA_TYPES.include?(media_type)
|
FORM_DATA_MEDIA_TYPES.include?(media_type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def body_stream #:nodoc:
|
def body_stream # :nodoc:
|
||||||
get_header("rack.input")
|
get_header("rack.input")
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -360,7 +360,7 @@ def reset_session
|
|||||||
session.destroy
|
session.destroy
|
||||||
end
|
end
|
||||||
|
|
||||||
def session=(session) #:nodoc:
|
def session=(session) # :nodoc:
|
||||||
Session.set self, session
|
Session.set self, session
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -336,7 +336,7 @@ def body=(body)
|
|||||||
# Avoid having to pass an open file handle as the response body.
|
# Avoid having to pass an open file handle as the response body.
|
||||||
# Rack::Sendfile will usually intercept the response and uses
|
# Rack::Sendfile will usually intercept the response and uses
|
||||||
# the path directly, so there is no reason to open the file.
|
# the path directly, so there is no reason to open the file.
|
||||||
class FileBody #:nodoc:
|
class FileBody # :nodoc:
|
||||||
attr_reader :to_path
|
attr_reader :to_path
|
||||||
|
|
||||||
def initialize(path)
|
def initialize(path)
|
||||||
|
@ -270,9 +270,10 @@ def port
|
|||||||
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
|
# req = ActionDispatch::Request.new 'HTTP_HOST' => 'example.com:8080'
|
||||||
# req.standard_port # => 80
|
# req.standard_port # => 80
|
||||||
def standard_port
|
def standard_port
|
||||||
case protocol
|
if "https://" == protocol
|
||||||
when "https://" then 443
|
443
|
||||||
else 80
|
else
|
||||||
|
80
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user