Merge branch 'main' into less-initializers

This commit is contained in:
Rafael Mendonça França 2021-09-15 17:18:49 -04:00
commit fd41ea1f2d
No known key found for this signature in database
GPG Key ID: FC23B6D0F1EEE948
909 changed files with 24175 additions and 11566 deletions

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/

@ -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

@ -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

@ -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.

@ -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
}); });
}); }));

@ -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 };

@ -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

@ -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.

@ -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